Skip to content

Commit 8081fe8

Browse files
committed
feat: add authentication for TPA
Signed-off-by: Ruben Romero Montes <[email protected]>
1 parent edd02d6 commit 8081fe8

File tree

14 files changed

+25959
-55
lines changed

14 files changed

+25959
-55
lines changed

src/main/java/com/redhat/exhort/integration/Constants.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ private Constants() {}
4646
public static final String SNYK_USER_AGENT_HEADER_FORMAT = "redhat-snyk-%s-%s";
4747
public static final String OSS_INDEX_USER_HEADER = "ex-oss-index-user";
4848
public static final String OSS_INDEX_TOKEN_HEADER = "ex-oss-index-token";
49+
public static final String TPA_TOKEN_HEADER = "ex-tpa-token";
50+
4951
public static final String VERBOSE_MODE_HEADER = "verbose";
5052

5153
public static final String RHDA_TOKEN_HEADER = "rhda-token";
@@ -96,15 +98,16 @@ private Constants() {}
9698

9799
public static final String SNYK_DEP_GRAPH_API_PATH = "/test/dep-graph";
98100
public static final String SNYK_TOKEN_API_PATH = "/user/me";
101+
99102
public static final String OSS_INDEX_AUTH_COMPONENT_API_PATH = "/authorized/component-report";
100103
public static final String OSS_INDEX_VERSION_PATH = "/version";
101104
public static final String OSV_NVD_PURLS_PATH = "/purls";
102-
103105
public static final String OSV_NVD_HEALTH_PATH = "/q/health";
104106

105107
public static final String TRUSTED_CONTENT_PATH = "/recommend";
106108
public static final String TPA_ANALYZE_PATH = "/vulnerability/analyze";
107109
public static final String TPA_HEALTH_PATH = "/health/live";
110+
public static final String TPA_TOKEN_PATH = "/vulnerability";
108111

109112
public static final String DEFAULT_ACCEPT_MEDIA_TYPE = MediaType.APPLICATION_JSON;
110113
public static final boolean DEFAULT_VERBOSE_MODE = false;

src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,11 @@ public void configure() {
244244
.setProperty(PROVIDERS_PARAM, constant(Arrays.asList(Constants.SNYK_PROVIDER)))
245245
.to(direct("snykValidateToken"))
246246
.when(header(Constants.OSS_INDEX_TOKEN_HEADER).isNotNull())
247-
.setProperty(PROVIDERS_PARAM, constant(Arrays.asList(Constants.OSS_INDEX_PROVIDER)))
247+
.setProperty(PROVIDERS_PARAM, constant(Arrays.asList(Constants.OSS_INDEX_PROVIDER)))
248248
.to(direct("ossValidateCredentials"))
249+
.when(header(Constants.TPA_TOKEN_HEADER).isNotNull())
250+
.setProperty(PROVIDERS_PARAM, constant(Arrays.asList(Constants.TPA_PROVIDER)))
251+
.to(direct("tpaValidateCredentials"))
249252
.otherwise()
250253
.setProperty(PROVIDERS_PARAM, constant(Arrays.asList(Constants.UNKNOWN_PROVIDER)))
251254
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(Response.Status.BAD_REQUEST.getStatusCode()))

src/main/java/com/redhat/exhort/integration/providers/tpa/TpaIntegration.java

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,39 @@ public class TpaIntegration extends EndpointRouteBuilder {
3939

4040
@Inject VulnerabilityProvider vulnerabilityProvider;
4141
@Inject TpaResponseHandler responseHandler;
42+
@Inject TpaRequestBuilder requestBuilder;
4243

4344
@Override
4445
public void configure() throws Exception {
4546
// fmt:off
4647
from(direct("tpaScan"))
4748
.routeId("tpaScan")
49+
.choice()
50+
.when(method(TpaRequestBuilder.class, "isEmpty"))
51+
.setBody(method(responseHandler, "emptyResponse"))
52+
.transform().method(responseHandler, "buildReport")
53+
.endChoice()
54+
.otherwise()
55+
.to(direct("tpaRequest"))
56+
.transform().method(responseHandler, "buildReport")
57+
.endChoice();
58+
59+
from(direct("tpaRequest"))
60+
.routeId("tpaRequest")
4861
.circuitBreaker()
4962
.faultToleranceConfiguration()
5063
.timeoutEnabled(true)
5164
.timeoutDuration(timeout)
5265
.end()
53-
.transform(method(TpaRequestBuilder.class, "buildRequest"))
54-
.to(direct("tpaRequest"))
66+
.transform(method(requestBuilder, "buildRequest"))
67+
.process(this::processRequest)
68+
.process(requestBuilder::addAuthentication)
69+
.to(http("{{api.tpa.host}}"))
70+
.transform().method(responseHandler, "responseToIssues")
71+
.endCircuitBreaker()
5572
.onFallback()
5673
.process(responseHandler::processResponseError)
57-
.end()
58-
.transform().method(responseHandler, "buildReport");
59-
60-
from(direct("tpaRequest"))
61-
.routeId("tpaRequest")
62-
.process(this::processRequest)
63-
.log("Headers: ${headers}")
64-
.to(http("{{api.tpa.host}}"))
65-
.transform().method(responseHandler, "responseToIssues");
74+
.end();
6675

6776
from(direct("tpaHealthCheck"))
6877
.routeId("tpaHealthCheck")
@@ -81,13 +90,28 @@ public void configure() throws Exception {
8190
.timeoutEnabled(true)
8291
.timeoutDuration(timeout)
8392
.end()
93+
.process(requestBuilder::addAuthentication)
8494
.to(http("{{api.tpa.management.host}}"))
8595
.setHeader(Exchange.HTTP_RESPONSE_TEXT,constant("Service is up and running"))
8696
.setBody(constant("Service is up and running"))
8797
.onFallback()
8898
.setBody(constant(Constants.TPA_PROVIDER + "Service is down"))
8999
.setHeader(Exchange.HTTP_RESPONSE_CODE,constant(Response.Status.SERVICE_UNAVAILABLE))
90100
.end();
101+
102+
from(direct("tpaValidateCredentials"))
103+
.routeId("tpaValidateCredentials")
104+
.circuitBreaker()
105+
.faultToleranceConfiguration()
106+
.timeoutEnabled(true)
107+
.timeoutDuration(timeout)
108+
.end()
109+
.process(this::processTokenRequest)
110+
.process(requestBuilder::addAuthentication)
111+
.to(http("{{api.tpa.host}}"))
112+
.setBody(constant("Token validated successfully"))
113+
.onFallback()
114+
.process(responseHandler::processTokenFallBack);
91115
// fmt:on
92116
}
93117

@@ -113,4 +137,15 @@ private void processHealthRequest(Exchange exchange) {
113137
message.setHeader(Exchange.HTTP_PATH, Constants.TPA_HEALTH_PATH);
114138
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.GET);
115139
}
140+
141+
private void processTokenRequest(Exchange exchange) {
142+
var message = exchange.getMessage();
143+
message.removeHeader(Exchange.HTTP_URI);
144+
message.removeHeader(Exchange.HTTP_HOST);
145+
message.removeHeader(Constants.ACCEPT_ENCODING_HEADER);
146+
message.removeHeader(Exchange.CONTENT_TYPE);
147+
message.setHeader(Exchange.HTTP_PATH, Constants.TPA_TOKEN_PATH);
148+
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.GET);
149+
message.setHeader(Exchange.HTTP_QUERY, "limit=0");
150+
}
116151
}

src/main/java/com/redhat/exhort/integration/providers/tpa/TpaRequestBuilder.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,28 @@
1818

1919
package com.redhat.exhort.integration.providers.tpa;
2020

21+
import java.util.Optional;
22+
23+
import org.apache.camel.Exchange;
24+
import org.eclipse.microprofile.config.inject.ConfigProperty;
25+
2126
import com.fasterxml.jackson.core.JsonProcessingException;
2227
import com.fasterxml.jackson.databind.ObjectMapper;
2328
import com.redhat.exhort.config.ObjectMapperProducer;
29+
import com.redhat.exhort.integration.Constants;
2430
import com.redhat.exhort.model.DependencyTree;
2531

2632
import io.quarkus.runtime.annotations.RegisterForReflection;
2733

34+
import jakarta.enterprise.context.ApplicationScoped;
35+
36+
@ApplicationScoped
2837
@RegisterForReflection
2938
public class TpaRequestBuilder {
3039

40+
@ConfigProperty(name = "api.tpa.token")
41+
Optional<String> defaultToken;
42+
3143
private ObjectMapper mapper = ObjectMapperProducer.newInstance();
3244

3345
public String buildRequest(DependencyTree tree) throws JsonProcessingException {
@@ -37,4 +49,22 @@ public String buildRequest(DependencyTree tree) throws JsonProcessingException {
3749
request.set("purls", purls);
3850
return mapper.writeValueAsString(request);
3951
}
52+
53+
public void addAuthentication(Exchange exchange) {
54+
var message = exchange.getMessage();
55+
var userToken = message.getHeader(Constants.TPA_TOKEN_HEADER, String.class);
56+
String token = null;
57+
if (userToken != null) {
58+
token = userToken;
59+
} else if (defaultToken.isPresent()) {
60+
token = defaultToken.get();
61+
}
62+
if (token != null) {
63+
message.setHeader("Authorization", "Bearer " + token);
64+
}
65+
}
66+
67+
public boolean isEmpty(DependencyTree tree) {
68+
return tree.dependencies().isEmpty();
69+
}
4070
}

src/test/java/com/redhat/exhort/extensions/WiremockExtension.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
public class WiremockExtension implements QuarkusTestResourceLifecycleManager {
3030

3131
public static final String SNYK_TOKEN = "snyk-token-xyz";
32+
public static final String TPA_TOKEN = "tpa-token-abc";
3233

3334
private final WireMockServer server = new WireMockServer(options().dynamicPort());
3435

@@ -42,7 +43,8 @@ public Map<String, String> start() {
4243
"api.trustedcontent.host", server.baseUrl(),
4344
"api.ossindex.host", server.baseUrl(),
4445
"api.onguard.host", server.baseUrl(),
45-
"api.tpa.host", server.baseUrl());
46+
"api.tpa.host", server.baseUrl(),
47+
"api.tpa.token", TPA_TOKEN);
4648
}
4749

4850
@Override

src/test/java/com/redhat/exhort/integration/AbstractAnalysisTest.java

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
3131
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
3232
import static com.redhat.exhort.extensions.WiremockExtension.SNYK_TOKEN;
33+
import static com.redhat.exhort.extensions.WiremockExtension.TPA_TOKEN;
3334
import static org.junit.jupiter.api.Assertions.assertEquals;
3435
import static org.junit.jupiter.api.Assertions.assertFalse;
3536
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -198,6 +199,7 @@ protected void verifyTokenRequest(String provider, Map<String, String> headers)
198199
case Constants.OSS_INDEX_PROVIDER -> verifyOssRequest(
199200
headers.get(Constants.OSS_INDEX_USER_HEADER),
200201
headers.get(Constants.OSS_INDEX_TOKEN_HEADER));
202+
case Constants.TPA_PROVIDER -> verifyTpaTokenRequest(headers.get(Constants.TPA_TOKEN_HEADER));
201203
}
202204
}
203205

@@ -213,6 +215,36 @@ protected void verifySnykTokenRequest(String token) {
213215
}
214216
}
215217

218+
protected void verifyTpaTokenRequest(String token) {
219+
if (token == null) {
220+
server.verify(
221+
1,
222+
getRequestedFor(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
223+
.withQueryParam("limit", equalTo("0")));
224+
} else {
225+
server.verify(
226+
1,
227+
getRequestedFor(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
228+
.withQueryParam("limit", equalTo("0"))
229+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + token)));
230+
}
231+
}
232+
233+
protected void verifyTpaRequest(String token) {
234+
verifyTpaRequest(token, 1);
235+
}
236+
237+
protected void verifyTpaRequest(String token, int count) {
238+
if (token == null) {
239+
server.verify(count, postRequestedFor(urlEqualTo(Constants.TPA_ANALYZE_PATH)));
240+
} else {
241+
server.verify(
242+
count,
243+
postRequestedFor(urlEqualTo(Constants.TPA_ANALYZE_PATH))
244+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + token)));
245+
}
246+
}
247+
216248
protected void stubAllProviders() {
217249
stubSnykRequests();
218250
stubOssToken();
@@ -232,7 +264,8 @@ protected void verifyProviders(Collection<String> providers, Map<String, String>
232264
credentials.get(Constants.OSS_INDEX_USER_HEADER),
233265
credentials.get(Constants.OSS_INDEX_TOKEN_HEADER));
234266
case Constants.OSV_PROVIDER -> verifyOsvNvdRequest();
235-
case Constants.TPA_PROVIDER -> verifyTpaRequest();
267+
case Constants.TPA_PROVIDER -> verifyTpaRequest(
268+
credentials.get(Constants.TPA_TOKEN_HEADER));
236269
}
237270
});
238271
verifyTrustedContentRequest();
@@ -352,8 +385,25 @@ protected void stubOsvRequests() {
352385
}
353386

354387
protected void stubTpaRequests() {
388+
// Missing token
389+
server.stubFor(post(Constants.TPA_ANALYZE_PATH).willReturn(aResponse().withStatus(401)));
390+
391+
// Invalid token
355392
server.stubFor(
356393
post(Constants.TPA_ANALYZE_PATH)
394+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + INVALID_TOKEN))
395+
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
396+
.willReturn(
397+
aResponse()
398+
.withStatus(401)
399+
.withBody(
400+
"{\"error\": \"Unauthorized\", \"message\": \"Authentication failed\"}")));
401+
402+
server.stubFor(
403+
post(Constants.TPA_ANALYZE_PATH)
404+
.withHeader(
405+
Constants.AUTHORIZATION_HEADER,
406+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
357407
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
358408
.willReturn(
359409
aResponse()
@@ -363,6 +413,9 @@ protected void stubTpaRequests() {
363413

364414
server.stubFor(
365415
post(Constants.TPA_ANALYZE_PATH)
416+
.withHeader(
417+
Constants.AUTHORIZATION_HEADER,
418+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
366419
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
367420
.withRequestBody(
368421
equalToJson(loadFileAsString("__files/tpa/maven_request.json"), true, false))
@@ -373,6 +426,9 @@ protected void stubTpaRequests() {
373426
.withBodyFile("tpa/maven_report.json")));
374427
server.stubFor(
375428
post(Constants.TPA_ANALYZE_PATH)
429+
.withHeader(
430+
Constants.AUTHORIZATION_HEADER,
431+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
376432
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
377433
.withRequestBody(
378434
equalToJson(loadFileAsString("__files/tpa/batch_request.json"), true, false))
@@ -383,6 +439,42 @@ protected void stubTpaRequests() {
383439
.withBodyFile("tpa/maven_report.json")));
384440
}
385441

442+
protected void stubTpaTokenRequests() {
443+
// Missing token
444+
server.stubFor(
445+
get(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
446+
.withQueryParam("limit", equalTo("0"))
447+
.willReturn(aResponse().withStatus(401)));
448+
// Default request
449+
server.stubFor(
450+
get(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
451+
.withHeader(
452+
Constants.AUTHORIZATION_HEADER,
453+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
454+
.withQueryParam("limit", equalTo("0"))
455+
.willReturn(
456+
aResponse()
457+
.withStatus(200)
458+
.withHeader(Exchange.CONTENT_TYPE, MediaType.APPLICATION_JSON)
459+
.withBodyFile("tpa/empty_report.json")));
460+
// Internal Error
461+
server.stubFor(
462+
get(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
463+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + ERROR_TOKEN))
464+
.withQueryParam("limit", equalTo("0"))
465+
.willReturn(aResponse().withStatus(500).withBody("This is an example error")));
466+
// Invalid token
467+
server.stubFor(
468+
get(urlPathEqualTo(Constants.TPA_TOKEN_PATH))
469+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + INVALID_TOKEN))
470+
.withQueryParam("limit", equalTo("0"))
471+
.willReturn(
472+
aResponse()
473+
.withStatus(401)
474+
.withBody(
475+
"{\"error\": \"Unauthorized\", \"message\": \"Authentication failed\"}")));
476+
}
477+
386478
protected void verifyTrustedContentRequest() {
387479
server.verify(1, postRequestedFor(urlEqualTo(Constants.TRUSTED_CONTENT_PATH)));
388480
}
@@ -612,14 +704,6 @@ protected void verifyOsvNvdRequest(int count) {
612704
server.verify(count, postRequestedFor(urlEqualTo(Constants.OSV_NVD_PURLS_PATH)));
613705
}
614706

615-
protected void verifyTpaRequest() {
616-
verifyTpadRequest(1);
617-
}
618-
619-
protected void verifyTpadRequest(int count) {
620-
server.verify(count, postRequestedFor(urlEqualTo(Constants.TPA_ANALYZE_PATH)));
621-
}
622-
623707
protected void verifyNoInteractions() {
624708
verifyNoInteractionsWithSnyk();
625709
verifyNoInteractionsWithOSS();

0 commit comments

Comments
 (0)