Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Repository Bearer Authentication #4483

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/_docs/datasources/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ for information on Package URL and the various ways it is used throughout Depend

### Authentication

For each repository a Username and Password can be specified to perform Basic Authentication.
To use Bearer Token authentication, leave the Username field empty and fill out the Token in the Password field.

#### GitHub

For GitHub repositories (`github.com` per default), the username should be the GitHub account's username,
and the password should be a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
(PAT) with public access (no additional scopes).
(PAT) with public access (no additional scopes).
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@
*/
package org.dependencytrack.persistence;

import alpine.common.logging.Logger;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import alpine.security.crypto.DataEncryption;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.model.Repository;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -37,6 +26,19 @@
import java.util.Map;
import java.util.UUID;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.model.Repository;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;

import alpine.common.logging.Logger;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import alpine.security.crypto.DataEncryption;

public class RepositoryQueryManager extends QueryManager implements IQueryManager {
private static final Logger LOGGER = Logger.getLogger(RepositoryQueryManager.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ protected CloseableHttpResponse processHttpRequest(String url) throws IOExceptio
URIBuilder uriBuilder = new URIBuilder(url);
final HttpUriRequest request = new HttpGet(uriBuilder.build().toString());
request.addHeader("accept", "application/json");
if (username != null || password != null) {
if (!StringUtils.isEmpty(username)) { // for some reason there is a testcase for password being null
request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(username, password));
} else if (!StringUtils.isEmpty(password)) {
request.addHeader("Authorization", "Bearer " + password);
}
return HttpClientPool.getClient().execute(request);
} catch (URISyntaxException ex) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/dependencytrack/util/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
*/
package org.dependencytrack.util;

import static org.apache.http.HttpHeaders.AUTHORIZATION;

import java.util.Base64;
import java.util.Objects;

import static org.apache.http.HttpHeaders.AUTHORIZATION;

public final class HttpUtil {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
*/
package org.dependencytrack.resources.v1;

import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import java.util.Date;
import java.util.List;

import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.Repository;
Expand All @@ -33,13 +34,13 @@
import org.junit.ClassRule;
import org.junit.Test;

import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Date;
import java.util.List;

public class RepositoryResourceTest extends ResourceTest {

Expand Down Expand Up @@ -173,7 +174,7 @@ public void getRepositoryMetaUntrackedComponentTest() {


@Test
public void createRepositoryTest() {
public void createRepositoryTestWithBasicAuth() {
Repository repository = new Repository();
repository.setAuthenticationRequired(true);
repository.setEnabled(true);
Expand Down Expand Up @@ -203,6 +204,40 @@ public void createRepositoryTest() {
Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled"));
}

@Test
public void createRepositoryTestWithBearerAuth() {
//Password field gets ignored during json serialization, so create the json ourselves
String repo = """
{
"identifier":"test2",
"url":"https://www.foobar2.com",
"internal":true,
"authenticationRequired":true,
"password":"letoken",
"enabled":true,
"type":"MAVEN"
}
""";

Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey)
.put(Entity.entity(repo, MediaType.APPLICATION_JSON));
Assert.assertEquals(201, response.getStatus());

response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class);
Assert.assertEquals(200, response.getStatus(), 0);
Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER));
JsonArray json = parseJsonArray(response);
Assert.assertNotNull(json);
Assert.assertEquals(18, json.size());
Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type"));
Assert.assertEquals("test2", json.getJsonObject(13).getString("identifier"));
Assert.assertEquals("https://www.foobar2.com", json.getJsonObject(13).getString("url"));
Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0);
Assert.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired"));
Assert.assertFalse(json.getJsonObject(13).containsKey("username"));
Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled"));
}

@Test
public void createNonInternalRepositoryTest() {
Repository repository = new Repository();
Expand Down Expand Up @@ -262,7 +297,6 @@ public void createRepositoryAuthFalseTest() {
Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0);
Assert.assertFalse(json.getJsonObject(13).getBoolean("authenticationRequired"));
Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled"));

}

@Test
Expand Down Expand Up @@ -298,6 +332,5 @@ public void updateRepositoryTest() throws Exception {
}
}
}

}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.dependencytrack.tasks;

import alpine.event.framework.EventService;
import com.github.packageurl.PackageURL;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.http.Body;
import com.github.tomakehurst.wiremock.http.ContentTypeHeader;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.event.RepositoryMetaEvent;
import org.dependencytrack.model.Component;
Expand All @@ -19,12 +19,14 @@
import org.junit.Rule;
import org.junit.Test;

import jakarta.ws.rs.core.MediaType;
import java.util.List;
import com.github.packageurl.PackageURL;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.http.Body;
import com.github.tomakehurst.wiremock.http.ContentTypeHeader;
import com.github.tomakehurst.wiremock.junit.WireMockRule;

import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import alpine.event.framework.EventService;
import jakarta.ws.rs.core.MediaType;

public class RepoMetaAnalysisTaskTest extends PersistenceCapableTest {

Expand Down Expand Up @@ -70,7 +72,7 @@ public void informTestNullPassword() throws Exception {
</versions>
<lastUpdated>20210213164433</lastUpdated>
</versioning>
</metadata>
</metadata>
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
)
.withHeader("X-CheckSum-MD5", "md5hash")
Expand All @@ -94,7 +96,7 @@ public void informTestNullPassword() throws Exception {

@Test
public void informTestNullUserName() throws Exception {
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic"))
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Bearer"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withResponseBody(Body.ofBinaryOrText("""
Expand All @@ -116,7 +118,7 @@ public void informTestNullUserName() throws Exception {
</versions>
<lastUpdated>20210213164433</lastUpdated>
</versioning>
</metadata>
</metadata>
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
)
.withHeader("X-CheckSum-MD5", "md5hash")
Expand Down Expand Up @@ -162,7 +164,7 @@ public void informTestNullUserNameAndPassword() throws Exception {
</versions>
<lastUpdated>20210213164433</lastUpdated>
</versioning>
</metadata>
</metadata>
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
)
.withHeader("X-CheckSum-MD5", "md5hash")
Expand All @@ -186,7 +188,7 @@ public void informTestNullUserNameAndPassword() throws Exception {

@Test
public void informTestUserNameAndPassword() throws Exception {
WireMock.stubFor(WireMock.get(WireMock.anyUrl())
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withResponseBody(Body.ofBinaryOrText("""
Expand All @@ -208,7 +210,7 @@ public void informTestUserNameAndPassword() throws Exception {
</versions>
<lastUpdated>20210213164433</lastUpdated>
</versioning>
</metadata>
</metadata>
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
)
.withHeader("X-CheckSum-MD5", "md5hash")
Expand All @@ -229,4 +231,51 @@ public void informTestUserNameAndPassword() throws Exception {
qm.getPersistenceManager().refresh(metaComponent);
assertThat(metaComponent.getLatestVersion()).isEqualTo("4.13.2");
}

@Test
public void informTestBearerToken() throws Exception {
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Bearer"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withResponseBody(Body.ofBinaryOrText("""
<metadata>
<groupId>test4</groupId>
<artifactId>test4</artifactId>
<versioning>
<latest>5.13.2</latest>
<release>5.13.2</release>
<versions>
<version>5.13-beta-1</version>
<version>5.13-beta-2</version>
<version>5.13-beta-3</version>
<version>5.13-rc-1</version>
<version>5.13-rc-2</version>
<version>5.13</version>
<version>5.13.1</version>
<version>5.13.2</version>
</versions>
<lastUpdated>20210213164433</lastUpdated>
</versioning>
</metadata>
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
)
.withHeader("X-CheckSum-MD5", "md5hash")
.withHeader("X-Checksum-SHA1", "sha1hash")
.withHeader("X-Checksum-SHA512", "sha512hash")
.withHeader("X-Checksum-SHA256", "sha256hash")
.withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT")));
EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false);
Component component = new Component();
component.setProject(project);
component.setName("test3");
component.setPurl(new PackageURL("pkg:maven/test4/[email protected]"));
qm.createComponent(component, false);
qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, null, "testPassword");
new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component)));
RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test4", "test4");
qm.getPersistenceManager().refresh(metaComponent);
assertThat(metaComponent.getLatestVersion()).isEqualTo("5.13.2");
}

}
Loading
Loading