Skip to content

Commit 4411d7f

Browse files
authored
feat: Migrate Reactive Routes to Vert.X Web and add security tests (#838)
2 parents c7e04c6 + 815d07b commit 4411d7f

34 files changed

Lines changed: 1716 additions & 161 deletions

File tree

client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
public class AuthInterceptor extends ClientCallInterceptor {
2525

2626
private static final String BEARER_SCHEME = "bearer";
27+
private static final String BASIC_SCHEME = "basic";
2728
public static final String AUTHORIZATION = "Authorization";
2829
private static final String BEARER = "Bearer ";
30+
private static final String BASIC = "Basic ";
2931
private final CredentialService credentialService;
3032

3133
public AuthInterceptor(final CredentialService credentialService) {
@@ -51,9 +53,13 @@ public PayloadAndHeaders intercept(String methodName, @Nullable Object payload,
5153
continue;
5254
}
5355
if (securityScheme instanceof HTTPAuthSecurityScheme httpAuthSecurityScheme) {
54-
if (httpAuthSecurityScheme.scheme().toLowerCase(Locale.ROOT).equals(BEARER_SCHEME)) {
56+
String scheme = httpAuthSecurityScheme.scheme().toLowerCase(Locale.ROOT);
57+
if (scheme.equals(BEARER_SCHEME)) {
5558
updatedHeaders.put(AUTHORIZATION, getBearerValue(credential));
5659
return new PayloadAndHeaders(payload, updatedHeaders);
60+
} else if (scheme.equals(BASIC_SCHEME)) {
61+
updatedHeaders.put(AUTHORIZATION, getBasicValue(credential));
62+
return new PayloadAndHeaders(payload, updatedHeaders);
5763
}
5864
} else if (securityScheme instanceof OAuth2SecurityScheme
5965
|| securityScheme instanceof OpenIdConnectSecurityScheme) {
@@ -72,4 +78,8 @@ public PayloadAndHeaders intercept(String methodName, @Nullable Object payload,
7278
private static String getBearerValue(String credential) {
7379
return BEARER + credential;
7480
}
81+
82+
private static String getBasicValue(String credential) {
83+
return BASIC + credential;
84+
}
7585
}

client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptorTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,21 @@ public void testBearerSecurityScheme() {
129129
testSecurityScheme(authTestCase);
130130
}
131131

132+
@Test
133+
public void testBasicAuthSecurityScheme() {
134+
// Credential should be pre-encoded base64("testuser:testpass") = "dGVzdHVzZXI6dGVzdHBhc3M="
135+
AuthTestCase authTestCase = new AuthTestCase(
136+
"http://agent.com/rpc",
137+
"session-id",
138+
"basic",
139+
"dGVzdHVzZXI6dGVzdHBhc3M=",
140+
new HTTPAuthSecurityScheme(null, "basic", "HTTP Basic authentication"),
141+
"Authorization",
142+
"Basic dGVzdHVzZXI6dGVzdHBhc3M="
143+
);
144+
testSecurityScheme(authTestCase);
145+
}
146+
132147
private void testSecurityScheme(AuthTestCase authTestCase) {
133148
credentialStore.setCredential(authTestCase.sessionId, authTestCase.schemeName, authTestCase.credential);
134149

extras/opentelemetry/integration-tests/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
<artifactId>quarkus-opentelemetry</artifactId>
4141
</dependency>
4242

43-
<!-- Quarkus Reactive Routes (for test utilities) -->
43+
<!-- Quarkus Vertx HTTP (for test utilities) -->
4444
<dependency>
4545
<groupId>io.quarkus</groupId>
46-
<artifactId>quarkus-reactive-routes</artifactId>
46+
<artifactId>quarkus-vertx-http</artifactId>
4747
</dependency>
4848

4949
<!-- CDI -->

extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutes.java

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
import io.opentelemetry.context.Scope;
1919
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
2020
import io.opentelemetry.sdk.trace.data.SpanData;
21-
import io.quarkus.vertx.web.Body;
22-
import io.quarkus.vertx.web.Param;
23-
import io.quarkus.vertx.web.Route;
21+
import io.vertx.ext.web.Router;
2422
import io.vertx.ext.web.RoutingContext;
23+
import io.vertx.ext.web.handler.BodyHandler;
2524
import jakarta.enterprise.context.ApplicationScoped;
25+
import jakarta.enterprise.event.Observes;
2626
import jakarta.enterprise.inject.Produces;
2727
import jakarta.inject.Inject;
2828
import jakarta.inject.Singleton;
@@ -48,8 +48,47 @@ public class A2ATestRoutes {
4848
@Inject
4949
Tracer tracer;
5050

51-
@Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING)
52-
public void saveTask(@Body String body, RoutingContext rc) {
51+
void setupRoutes(@Observes Router router) {
52+
router.post("/test/task")
53+
.consumes(APPLICATION_JSON)
54+
.blockingHandler(ctx -> saveTask(ctx.body().asString(), ctx));
55+
56+
router.get("/test/task/:taskId")
57+
.produces(APPLICATION_JSON)
58+
.blockingHandler(ctx -> getTask(ctx.pathParam("taskId"), ctx));
59+
60+
router.delete("/test/task/:taskId")
61+
.blockingHandler(ctx -> deleteTask(ctx.pathParam("taskId"), ctx));
62+
63+
router.post("/test/queue/ensure/:taskId")
64+
.handler(ctx -> ensureTaskQueue(ctx.pathParam("taskId"), ctx));
65+
66+
router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId")
67+
.handler(BodyHandler.create())
68+
.handler(ctx -> enqueueTaskStatusUpdateEvent(ctx.pathParam("taskId"), ctx.body().asString(), ctx));
69+
70+
router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId")
71+
.handler(BodyHandler.create())
72+
.handler(ctx -> enqueueTaskArtifactUpdateEvent(ctx.pathParam("taskId"), ctx.body().asString(), ctx));
73+
74+
router.get("/test/queue/childCount/:taskId")
75+
.produces(TEXT_PLAIN)
76+
.handler(ctx -> getChildQueueCount(ctx.pathParam("taskId"), ctx));
77+
78+
router.get("/hello")
79+
.produces(TEXT_PLAIN)
80+
.handler(ctx -> hello(ctx));
81+
82+
router.get("/export")
83+
.produces(APPLICATION_JSON)
84+
.handler(ctx -> exportSpans(ctx));
85+
86+
router.get("/reset")
87+
.produces(TEXT_PLAIN)
88+
.handler(ctx -> reset(ctx));
89+
}
90+
91+
public void saveTask(String body, RoutingContext rc) {
5392
try {
5493
Task task = JsonUtil.fromJson(body, Task.class);
5594
testUtilsBean.saveTask(task);
@@ -61,8 +100,7 @@ public void saveTask(@Body String body, RoutingContext rc) {
61100
}
62101
}
63102

64-
@Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.GET}, produces = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING)
65-
public void getTask(@Param String taskId, RoutingContext rc) {
103+
public void getTask(String taskId, RoutingContext rc) {
66104
try {
67105
Task task = testUtilsBean.getTask(taskId);
68106
if (task == null) {
@@ -81,8 +119,7 @@ public void getTask(@Param String taskId, RoutingContext rc) {
81119
}
82120
}
83121

84-
@Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING)
85-
public void deleteTask(@Param String taskId, RoutingContext rc) {
122+
public void deleteTask(String taskId, RoutingContext rc) {
86123
try {
87124
Task task = testUtilsBean.getTask(taskId);
88125
if (task == null) {
@@ -100,8 +137,7 @@ public void deleteTask(@Param String taskId, RoutingContext rc) {
100137
}
101138
}
102139

103-
@Route(path = "/test/queue/ensure/:taskId", methods = {Route.HttpMethod.POST})
104-
public void ensureTaskQueue(@Param String taskId, RoutingContext rc) {
140+
public void ensureTaskQueue(String taskId, RoutingContext rc) {
105141
try {
106142
testUtilsBean.ensureQueue(taskId);
107143
rc.response()
@@ -112,8 +148,7 @@ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) {
112148
}
113149
}
114150

115-
@Route(path = "/test/queue/enqueueTaskStatusUpdateEvent/:taskId", methods = {Route.HttpMethod.POST})
116-
public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) {
151+
public void enqueueTaskStatusUpdateEvent(String taskId, String body, RoutingContext rc) {
117152
try {
118153
TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class);
119154
testUtilsBean.enqueueEvent(taskId, event);
@@ -125,8 +160,7 @@ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body
125160
}
126161
}
127162

128-
@Route(path = "/test/queue/enqueueTaskArtifactUpdateEvent/:taskId", methods = {Route.HttpMethod.POST})
129-
public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) {
163+
public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingContext rc) {
130164
try {
131165
TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class);
132166
testUtilsBean.enqueueEvent(taskId, event);
@@ -138,15 +172,13 @@ public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String bo
138172
}
139173
}
140174

141-
@Route(path = "/test/queue/childCount/:taskId", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
142-
public void getChildQueueCount(@Param String taskId, RoutingContext rc) {
175+
public void getChildQueueCount(String taskId, RoutingContext rc) {
143176
int count = testUtilsBean.getChildQueueCount(taskId);
144177
rc.response()
145178
.setStatusCode(200)
146179
.end(String.valueOf(count));
147180
}
148181

149-
@Route(path = "/hello", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
150182
public void hello(RoutingContext rc) {
151183
Span span = tracer.spanBuilder("hello").startSpan();
152184
try (Scope scope = span.makeCurrent()) {
@@ -159,8 +191,7 @@ public void hello(RoutingContext rc) {
159191
}
160192
}
161193

162-
@Route(path = "/export", methods = {Route.HttpMethod.GET}, produces = {APPLICATION_JSON})
163-
public void exportSpans(@Param String taskId, RoutingContext rc) {
194+
public void exportSpans(RoutingContext rc) {
164195
List<SpanData> spans = inMemorySpanExporter.getFinishedSpanItems()
165196
.stream()
166197
.filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset"))
@@ -202,8 +233,7 @@ private JsonElement serialize(List<SpanData> spanDatas) {
202233
return spans;
203234
}
204235

205-
@Route(path = "/reset", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
206-
public void reset(@Param String taskId, RoutingContext rc) {
236+
public void reset(RoutingContext rc) {
207237
inMemorySpanExporter.reset();
208238
rc.response().setStatusCode(200).end();
209239
}

extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ABaseTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.a2aproject.sdk.extras.opentelemetry.it;
22

3-
import static io.quarkus.vertx.web.ReactiveRoutes.APPLICATION_JSON;
43
import static io.restassured.RestAssured.given;
54
import static java.net.HttpURLConnection.HTTP_OK;
65
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -243,7 +242,7 @@ protected void saveTaskInTaskStore(Task task) throws Exception {
243242
HttpRequest request = HttpRequest.newBuilder()
244243
.uri(URI.create("http://localhost:" + serverPort + "/test/task"))
245244
.POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task)))
246-
.header("Content-Type", APPLICATION_JSON)
245+
.header("Content-Type", "application/json")
247246
.build();
248247

249248
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

extras/push-notification-config-store-database-jpa/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,5 @@
103103
<artifactId>a2a-java-sdk-client</artifactId>
104104
<scope>test</scope>
105105
</dependency>
106-
<dependency>
107-
<groupId>io.quarkus</groupId>
108-
<artifactId>quarkus-reactive-routes</artifactId>
109-
<scope>test</scope>
110-
</dependency>
111106
</dependencies>
112107
</project>

extras/task-store-database-jpa/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,5 @@
102102
<artifactId>a2a-java-sdk-client</artifactId>
103103
<scope>test</scope>
104104
</dependency>
105-
<dependency>
106-
<groupId>io.quarkus</groupId>
107-
<artifactId>quarkus-reactive-routes</artifactId>
108-
<scope>test</scope>
109-
</dependency>
110105
</dependencies>
111106
</project>

http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,13 @@ public A2AHttpResponse get() throws IOException, InterruptedException {
267267
.build();
268268
HttpResponse<String> response =
269269
httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
270+
271+
if (response.statusCode() == HTTP_UNAUTHORIZED) {
272+
throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
273+
} else if (response.statusCode() == HTTP_FORBIDDEN) {
274+
throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
275+
}
276+
270277
return new JdkHttpResponse(response);
271278
}
272279

@@ -289,6 +296,13 @@ public A2AHttpResponse delete() throws IOException, InterruptedException {
289296
HttpRequest request = super.createRequestBuilder().DELETE().build();
290297
HttpResponse<String> response =
291298
httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
299+
300+
if (response.statusCode() == HTTP_UNAUTHORIZED) {
301+
throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
302+
} else if (response.statusCode() == HTTP_FORBIDDEN) {
303+
throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
304+
}
305+
292306
return new JdkHttpResponse(response);
293307
}
294308

reference/common/pom.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
</dependency>
3636
<dependency>
3737
<groupId>io.quarkus</groupId>
38-
<artifactId>quarkus-reactive-routes</artifactId>
38+
<artifactId>quarkus-vertx-http</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.quarkus</groupId>
42+
<artifactId>quarkus-arc</artifactId>
3943
</dependency>
4044
<dependency>
4145
<groupId>jakarta.enterprise</groupId>
@@ -73,5 +77,17 @@
7377
<artifactId>rest-assured</artifactId>
7478
<scope>test</scope>
7579
</dependency>
80+
<!-- Security dependencies - optional so they don't propagate transitively.
81+
Transport modules add these as test-scoped for auth tests. -->
82+
<dependency>
83+
<groupId>io.quarkus</groupId>
84+
<artifactId>quarkus-security</artifactId>
85+
<optional>true</optional>
86+
</dependency>
87+
<dependency>
88+
<groupId>io.quarkus</groupId>
89+
<artifactId>quarkus-elytron-security-properties-file</artifactId>
90+
<optional>true</optional>
91+
</dependency>
7692
</dependencies>
7793
</project>

0 commit comments

Comments
 (0)