Skip to content

Commit 7e3c0bd

Browse files
Add support for repository authentication with bearer token (#4483)
Co-authored-by: nscuro <[email protected]>
1 parent 19ca554 commit 7e3c0bd

File tree

7 files changed

+116
-29
lines changed

7 files changed

+116
-29
lines changed

docs/_docs/datasources/repositories.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ for information on Package URL and the various ways it is used throughout Depend
6161

6262
### Authentication
6363

64+
For each repository a Username and Password can be specified to perform Basic Authentication.
65+
To use Bearer Token authentication, leave the Username field empty and fill out the Token in the Password field.
66+
6467
#### GitHub
6568

6669
For GitHub repositories (`github.com` per default), the username should be the GitHub account's username,
6770
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)
68-
(PAT) with public access (no additional scopes).
71+
(PAT) with public access (no additional scopes).

src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@
1818
*/
1919
package org.dependencytrack.persistence;
2020

21-
import alpine.common.logging.Logger;
22-
import alpine.persistence.PaginatedResult;
23-
import alpine.resources.AlpineRequest;
24-
import alpine.security.crypto.DataEncryption;
25-
import org.apache.commons.lang3.StringUtils;
26-
import org.dependencytrack.model.Repository;
27-
import org.dependencytrack.model.RepositoryMetaComponent;
28-
import org.dependencytrack.model.RepositoryType;
29-
30-
import javax.jdo.PersistenceManager;
31-
import javax.jdo.Query;
3221
import java.io.Serializable;
3322
import java.util.ArrayList;
3423
import java.util.Collections;
@@ -37,6 +26,19 @@
3726
import java.util.Map;
3827
import java.util.UUID;
3928

29+
import javax.jdo.PersistenceManager;
30+
import javax.jdo.Query;
31+
32+
import org.apache.commons.lang3.StringUtils;
33+
import org.dependencytrack.model.Repository;
34+
import org.dependencytrack.model.RepositoryMetaComponent;
35+
import org.dependencytrack.model.RepositoryType;
36+
37+
import alpine.common.logging.Logger;
38+
import alpine.persistence.PaginatedResult;
39+
import alpine.resources.AlpineRequest;
40+
import alpine.security.crypto.DataEncryption;
41+
4042
public class RepositoryQueryManager extends QueryManager implements IQueryManager {
4143
private static final Logger LOGGER = Logger.getLogger(RepositoryQueryManager.class);
4244

src/main/java/org/dependencytrack/tasks/repositories/AbstractMetaAnalyzer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ protected CloseableHttpResponse processHttpRequest(String url) throws IOExceptio
117117
URIBuilder uriBuilder = new URIBuilder(url);
118118
final HttpUriRequest request = new HttpGet(uriBuilder.build().toString());
119119
request.addHeader("accept", "application/json");
120-
if (username != null || password != null) {
120+
if (!StringUtils.isEmpty(username)) { // for some reason there is a testcase for password being null
121121
request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(username, password));
122+
} else if (!StringUtils.isEmpty(password)) {
123+
request.addHeader("Authorization", "Bearer " + password);
122124
}
123125
return HttpClientPool.getClient().execute(request);
124126
} catch (URISyntaxException ex) {

src/main/java/org/dependencytrack/util/HttpUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
*/
1919
package org.dependencytrack.util;
2020

21+
import static org.apache.http.HttpHeaders.AUTHORIZATION;
22+
2123
import java.util.Base64;
2224
import java.util.Objects;
2325

24-
import static org.apache.http.HttpHeaders.AUTHORIZATION;
25-
2626
public final class HttpUtil {
2727

2828
/**

src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ void getRepositoryMetaUntrackedComponentTest() {
172172

173173

174174
@Test
175-
void createRepositoryTest() {
175+
void createRepositoryTestWithBasicAuth() {
176176
Repository repository = new Repository();
177177
repository.setAuthenticationRequired(true);
178178
repository.setEnabled(true);
@@ -202,6 +202,40 @@ void createRepositoryTest() {
202202
Assertions.assertTrue(json.getJsonObject(13).getBoolean("enabled"));
203203
}
204204

205+
@Test
206+
public void createRepositoryTestWithBearerAuth() {
207+
//Password field gets ignored during json serialization, so create the json ourselves
208+
String repo = """
209+
{
210+
"identifier":"test2",
211+
"url":"https://www.foobar2.com",
212+
"internal":true,
213+
"authenticationRequired":true,
214+
"password":"letoken",
215+
"enabled":true,
216+
"type":"MAVEN"
217+
}
218+
""";
219+
220+
Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey)
221+
.put(Entity.entity(repo, MediaType.APPLICATION_JSON));
222+
Assertions.assertEquals(201, response.getStatus());
223+
224+
response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class);
225+
Assertions.assertEquals(200, response.getStatus(), 0);
226+
Assertions.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER));
227+
JsonArray json = parseJsonArray(response);
228+
Assertions.assertNotNull(json);
229+
Assertions.assertEquals(18, json.size());
230+
Assertions.assertEquals("MAVEN", json.getJsonObject(13).getString("type"));
231+
Assertions.assertEquals("test2", json.getJsonObject(13).getString("identifier"));
232+
Assertions.assertEquals("https://www.foobar2.com", json.getJsonObject(13).getString("url"));
233+
Assertions.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0);
234+
Assertions.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired"));
235+
Assertions.assertFalse(json.getJsonObject(13).containsKey("username"));
236+
Assertions.assertTrue(json.getJsonObject(13).getBoolean("enabled"));
237+
}
238+
205239
@Test
206240
void createNonInternalRepositoryTest() {
207241
Repository repository = new Repository();
@@ -261,7 +295,6 @@ void createRepositoryAuthFalseTest() {
261295
Assertions.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0);
262296
Assertions.assertFalse(json.getJsonObject(13).getBoolean("authenticationRequired"));
263297
Assertions.assertTrue(json.getJsonObject(13).getBoolean("enabled"));
264-
265298
}
266299

267300
@Test
@@ -297,6 +330,5 @@ void updateRepositoryTest() throws Exception {
297330
}
298331
}
299332
}
300-
301333
}
302334
}

src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
@WireMockTest
2929
class RepoMetaAnalysisTaskTest extends PersistenceCapableTest {
30+
3031
private WireMockRuntimeInfo wmRuntimeInfo;
3132

3233
@BeforeEach
@@ -67,7 +68,7 @@ void informTestNullPassword() throws Exception {
6768
</versions>
6869
<lastUpdated>20210213164433</lastUpdated>
6970
</versioning>
70-
</metadata>
71+
</metadata>
7172
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
7273
)
7374
.withHeader("X-CheckSum-MD5", "md5hash")
@@ -91,7 +92,7 @@ void informTestNullPassword() throws Exception {
9192

9293
@Test
9394
void informTestNullUserName() throws Exception {
94-
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic"))
95+
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Bearer"))
9596
.willReturn(WireMock.aResponse()
9697
.withStatus(200)
9798
.withResponseBody(Body.ofBinaryOrText("""
@@ -113,7 +114,7 @@ void informTestNullUserName() throws Exception {
113114
</versions>
114115
<lastUpdated>20210213164433</lastUpdated>
115116
</versioning>
116-
</metadata>
117+
</metadata>
117118
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
118119
)
119120
.withHeader("X-CheckSum-MD5", "md5hash")
@@ -159,7 +160,7 @@ void informTestNullUserNameAndPassword() throws Exception {
159160
</versions>
160161
<lastUpdated>20210213164433</lastUpdated>
161162
</versioning>
162-
</metadata>
163+
</metadata>
163164
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
164165
)
165166
.withHeader("X-CheckSum-MD5", "md5hash")
@@ -183,7 +184,7 @@ void informTestNullUserNameAndPassword() throws Exception {
183184

184185
@Test
185186
void informTestUserNameAndPassword() throws Exception {
186-
WireMock.stubFor(WireMock.get(WireMock.anyUrl())
187+
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Basic"))
187188
.willReturn(WireMock.aResponse()
188189
.withStatus(200)
189190
.withResponseBody(Body.ofBinaryOrText("""
@@ -205,7 +206,7 @@ void informTestUserNameAndPassword() throws Exception {
205206
</versions>
206207
<lastUpdated>20210213164433</lastUpdated>
207208
</versioning>
208-
</metadata>
209+
</metadata>
209210
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
210211
)
211212
.withHeader("X-CheckSum-MD5", "md5hash")
@@ -226,4 +227,51 @@ void informTestUserNameAndPassword() throws Exception {
226227
qm.getPersistenceManager().refresh(metaComponent);
227228
assertThat(metaComponent.getLatestVersion()).isEqualTo("4.13.2");
228229
}
230+
231+
@Test
232+
public void informTestBearerToken() throws Exception {
233+
WireMock.stubFor(WireMock.get(WireMock.anyUrl()).withHeader("Authorization", containing("Bearer"))
234+
.willReturn(WireMock.aResponse()
235+
.withStatus(200)
236+
.withResponseBody(Body.ofBinaryOrText("""
237+
<metadata>
238+
<groupId>test4</groupId>
239+
<artifactId>test4</artifactId>
240+
<versioning>
241+
<latest>5.13.2</latest>
242+
<release>5.13.2</release>
243+
<versions>
244+
<version>5.13-beta-1</version>
245+
<version>5.13-beta-2</version>
246+
<version>5.13-beta-3</version>
247+
<version>5.13-rc-1</version>
248+
<version>5.13-rc-2</version>
249+
<version>5.13</version>
250+
<version>5.13.1</version>
251+
<version>5.13.2</version>
252+
</versions>
253+
<lastUpdated>20210213164433</lastUpdated>
254+
</versioning>
255+
</metadata>
256+
""".getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON))
257+
)
258+
.withHeader("X-CheckSum-MD5", "md5hash")
259+
.withHeader("X-Checksum-SHA1", "sha1hash")
260+
.withHeader("X-Checksum-SHA512", "sha512hash")
261+
.withHeader("X-Checksum-SHA256", "sha256hash")
262+
.withHeader("Last-Modified", "Thu, 07 Jul 2022 14:00:00 GMT")));
263+
EventService.getInstance().subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class);
264+
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false);
265+
Component component = new Component();
266+
component.setProject(project);
267+
component.setName("test3");
268+
component.setPurl(new PackageURL("pkg:maven/test4/[email protected]"));
269+
qm.createComponent(component, false);
270+
qm.createRepository(RepositoryType.MAVEN, "test", wmRuntimeInfo.getHttpBaseUrl(), true, false, true, null, "testPassword");
271+
new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component)));
272+
RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test4", "test4");
273+
qm.getPersistenceManager().refresh(metaComponent);
274+
assertThat(metaComponent.getLatestVersion()).isEqualTo("5.13.2");
275+
}
276+
229277
}

src/test/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,22 @@ static void beforeClass() throws Exception {
5757
private static void setupMockServerClient(
5858
String path,
5959
String responseFile,
60-
String encodedBasicHeader
60+
String authHeader
6161
) throws Exception {
62-
setupMockServerClient(path, responseFile, encodedBasicHeader, "application/json", 200);
62+
setupMockServerClient(path, responseFile, authHeader, "application/json", 200);
6363
}
6464

6565
private static void setupMockServerClient(
6666
String path,
6767
String responseFile,
68-
String encodedBasicHeader,
68+
String authHeader,
6969
String contentType,
7070
int statusCode
7171
) throws Exception {
7272

7373
List<Header> headers = new ArrayList<>();
74-
if (encodedBasicHeader != null) {
75-
headers.add(new Header("Authorization", encodedBasicHeader));
74+
if (authHeader != null) {
75+
headers.add(new Header("Authorization", authHeader));
7676
}
7777

7878
new MockServerClient("localhost", 1080)

0 commit comments

Comments
 (0)