Skip to content

Commit 3eb557d

Browse files
committed
feat: add authentication for TPA
Signed-off-by: Ruben Romero Montes <[email protected]>
1 parent 05689aa commit 3eb557d

File tree

14 files changed

+25949
-55
lines changed

14 files changed

+25949
-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_QUERY);
144+
message.removeHeader(Exchange.HTTP_URI);
145+
message.removeHeader(Exchange.HTTP_HOST);
146+
message.removeHeader(Constants.ACCEPT_ENCODING_HEADER);
147+
message.removeHeader(Exchange.CONTENT_TYPE);
148+
message.setHeader(Exchange.HTTP_PATH, Constants.TPA_TOKEN_PATH);
149+
message.setHeader(Exchange.HTTP_METHOD, HttpMethod.GET);
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: 83 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,32 @@ protected void verifySnykTokenRequest(String token) {
213215
}
214216
}
215217

218+
protected void verifyTpaTokenRequest(String token) {
219+
if (token == null) {
220+
server.verify(1, getRequestedFor(urlEqualTo(Constants.TPA_TOKEN_PATH)));
221+
} else {
222+
server.verify(
223+
1,
224+
getRequestedFor(urlEqualTo(Constants.TPA_TOKEN_PATH))
225+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + token)));
226+
}
227+
}
228+
229+
protected void verifyTpaRequest(String token) {
230+
verifyTpaRequest(token, 1);
231+
}
232+
233+
protected void verifyTpaRequest(String token, int count) {
234+
if (token == null) {
235+
server.verify(count, postRequestedFor(urlEqualTo(Constants.TPA_ANALYZE_PATH)));
236+
} else {
237+
server.verify(
238+
count,
239+
postRequestedFor(urlEqualTo(Constants.TPA_ANALYZE_PATH))
240+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + token)));
241+
}
242+
}
243+
216244
protected void stubAllProviders() {
217245
stubSnykRequests();
218246
stubOssToken();
@@ -232,7 +260,8 @@ protected void verifyProviders(Collection<String> providers, Map<String, String>
232260
credentials.get(Constants.OSS_INDEX_USER_HEADER),
233261
credentials.get(Constants.OSS_INDEX_TOKEN_HEADER));
234262
case Constants.OSV_PROVIDER -> verifyOsvNvdRequest();
235-
case Constants.TPA_PROVIDER -> verifyTpaRequest();
263+
case Constants.TPA_PROVIDER -> verifyTpaRequest(
264+
credentials.get(Constants.TPA_TOKEN_HEADER));
236265
}
237266
});
238267
verifyTrustedContentRequest();
@@ -352,8 +381,25 @@ protected void stubOsvRequests() {
352381
}
353382

354383
protected void stubTpaRequests() {
384+
// Missing token
385+
server.stubFor(post(Constants.TPA_ANALYZE_PATH).willReturn(aResponse().withStatus(401)));
386+
387+
// Invalid token
388+
server.stubFor(
389+
post(Constants.TPA_ANALYZE_PATH)
390+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + INVALID_TOKEN))
391+
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
392+
.willReturn(
393+
aResponse()
394+
.withStatus(401)
395+
.withBody(
396+
"{\"error\": \"Unauthorized\", \"message\": \"Authentication failed\"}")));
397+
355398
server.stubFor(
356399
post(Constants.TPA_ANALYZE_PATH)
400+
.withHeader(
401+
Constants.AUTHORIZATION_HEADER,
402+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
357403
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
358404
.willReturn(
359405
aResponse()
@@ -363,6 +409,9 @@ protected void stubTpaRequests() {
363409

364410
server.stubFor(
365411
post(Constants.TPA_ANALYZE_PATH)
412+
.withHeader(
413+
Constants.AUTHORIZATION_HEADER,
414+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
366415
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
367416
.withRequestBody(
368417
equalToJson(loadFileAsString("__files/tpa/maven_request.json"), true, false))
@@ -373,6 +422,9 @@ protected void stubTpaRequests() {
373422
.withBodyFile("tpa/maven_report.json")));
374423
server.stubFor(
375424
post(Constants.TPA_ANALYZE_PATH)
425+
.withHeader(
426+
Constants.AUTHORIZATION_HEADER,
427+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
376428
.withHeader(Exchange.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON))
377429
.withRequestBody(
378430
equalToJson(loadFileAsString("__files/tpa/batch_request.json"), true, false))
@@ -383,6 +435,36 @@ protected void stubTpaRequests() {
383435
.withBodyFile("tpa/maven_report.json")));
384436
}
385437

438+
protected void stubTpaTokenRequests() {
439+
// Missing token
440+
server.stubFor(get(Constants.TPA_TOKEN_PATH).willReturn(aResponse().withStatus(401)));
441+
// Default request
442+
server.stubFor(
443+
get(Constants.TPA_TOKEN_PATH)
444+
.withHeader(
445+
Constants.AUTHORIZATION_HEADER,
446+
equalTo("Bearer " + TPA_TOKEN).or(equalTo("Bearer " + OK_TOKEN)))
447+
.willReturn(
448+
aResponse()
449+
.withStatus(200)
450+
.withHeader(Exchange.CONTENT_TYPE, MediaType.APPLICATION_JSON)
451+
.withBodyFile("tpa/empty_report.json")));
452+
// Internal Error
453+
server.stubFor(
454+
get(Constants.TPA_TOKEN_PATH)
455+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + ERROR_TOKEN))
456+
.willReturn(aResponse().withStatus(500).withBody("This is an example error")));
457+
// Invalid token
458+
server.stubFor(
459+
get(Constants.TPA_TOKEN_PATH)
460+
.withHeader(Constants.AUTHORIZATION_HEADER, equalTo("Bearer " + INVALID_TOKEN))
461+
.willReturn(
462+
aResponse()
463+
.withStatus(401)
464+
.withBody(
465+
"{\"error\": \"Unauthorized\", \"message\": \"Authentication failed\"}")));
466+
}
467+
386468
protected void verifyTrustedContentRequest() {
387469
server.verify(1, postRequestedFor(urlEqualTo(Constants.TRUSTED_CONTENT_PATH)));
388470
}
@@ -612,14 +694,6 @@ protected void verifyOsvNvdRequest(int count) {
612694
server.verify(count, postRequestedFor(urlEqualTo(Constants.OSV_NVD_PURLS_PATH)));
613695
}
614696

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-
623697
protected void verifyNoInteractions() {
624698
verifyNoInteractionsWithSnyk();
625699
verifyNoInteractionsWithOSS();

0 commit comments

Comments
 (0)