From 48ed4157cf56830a9b673fec4ecf3fe58040f7a9 Mon Sep 17 00:00:00 2001 From: ndr_brt Date: Mon, 16 Mar 2026 10:07:00 +0100 Subject: [PATCH] feat: implement oauth2 client credentials auth profile --- build.gradle.kts | 3 +- .../java/org/eclipse/dataplane/Dataplane.java | 21 +- .../domain/controlplane/ControlPlane.java | 11 +- .../domain/registration/Authorization.java | 17 +- .../registration/AuthorizationProfile.java | 68 ++++++ .../ControlPlaneRegistrationMessage.java | 2 +- .../Oauth2ClientCredentialsAuthorization.java | 72 +++++++ .../domain/registration/RawAuthorization.java | 41 ---- .../exception/AuthorizationNotSupported.java | 6 +- .../IllegalAttributeTypeException.java} | 15 +- .../org/eclipse/dataplane/ControlPlane.java | 51 +++-- .../org/eclipse/dataplane/DataplaneTest.java | 1 - .../org/eclipse/dataplane/HttpServer.java | 11 +- .../api/ControlPlaneRegistrationApiTest.java | 38 ++-- .../scenario/AuthorizationOauth2Test.java | 200 ++++++++++++++++++ .../dataplane/scenario/AuthorizationTest.java | 50 ++--- .../dataplane/scenario/ConsumerPullTest.java | 5 +- .../dataplane/scenario/ProviderPushTest.java | 5 +- .../dataplane/scenario/StreamingPullTest.java | 5 +- .../dataplane/scenario/StreamingPushTest.java | 5 +- 20 files changed, 463 insertions(+), 164 deletions(-) create mode 100644 src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationProfile.java create mode 100644 src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java delete mode 100644 src/main/java/org/eclipse/dataplane/domain/registration/RawAuthorization.java rename src/main/java/org/eclipse/dataplane/{domain/registration/AuthorizationType.java => port/exception/IllegalAttributeTypeException.java} (51%) create mode 100644 src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java diff --git a/build.gradle.kts b/build.gradle.kts index 309272f..d1e2632 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "org.eclipse.dataplane-core" -version = "0.0.6-SNAPSHOT" +version = "0.0.7-SNAPSHOT" repositories { mavenCentral() @@ -16,6 +16,7 @@ repositories { dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.21.1") + implementation("com.nimbusds:nimbus-jose-jwt:10.8") implementation("jakarta.ws.rs:jakarta.ws.rs-api:4.0.0") testImplementation(platform("org.junit:junit-bom:6.0.3")) diff --git a/src/main/java/org/eclipse/dataplane/Dataplane.java b/src/main/java/org/eclipse/dataplane/Dataplane.java index f1c8ee9..c1a7a45 100644 --- a/src/main/java/org/eclipse/dataplane/Dataplane.java +++ b/src/main/java/org/eclipse/dataplane/Dataplane.java @@ -28,7 +28,6 @@ import org.eclipse.dataplane.domain.dataflow.DataFlowSuspendMessage; import org.eclipse.dataplane.domain.dataflow.DataFlowTerminateMessage; import org.eclipse.dataplane.domain.registration.Authorization; -import org.eclipse.dataplane.domain.registration.AuthorizationType; import org.eclipse.dataplane.domain.registration.ControlPlaneRegistrationMessage; import org.eclipse.dataplane.domain.registration.DataPlaneRegistrationMessage; import org.eclipse.dataplane.logic.OnCompleted; @@ -56,7 +55,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.BiConsumer; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; import static java.util.Collections.emptyMap; @@ -79,7 +77,7 @@ public class Dataplane { private OnCompleted onCompleted = dataFlow -> Result.failure(new UnsupportedOperationException("onCompleted is not implemented")); private final HttpClient httpClient = HttpClient.newHttpClient(); - private final Map authorizationTypes = new HashMap<>(); + private final Map authorizations = new HashMap<>(); public static Builder newInstance() { return new Builder(); @@ -314,11 +312,10 @@ private Result notifyControlPlane(String action, DataFlow dataFlow, Object var controlPlane = controlPlaneStore.findByEndpoint(dataFlow.getCallbackAddress()); if (controlPlane.succeeded()) { - var authorization = controlPlane.getContent().authorization(); - if (authorization != null) { - var authorizationType = authorizationTypes.get(authorization.getType()); - var castAuthorization = objectMapper.convertValue(authorization, authorizationType.authorizationClass()); - authorizationType.authorizationFunction().accept(requestBuilder, castAuthorization); + var authorizationProfile = controlPlane.getContent().authorization(); + if (authorizationProfile != null) { + var authorization = authorizations.get(authorizationProfile.getType()); + authorization.apply(requestBuilder, authorizationProfile); } } @@ -348,7 +345,7 @@ public ControlPlaneStore controlPlaneStore() { public Result registerControlPlane(ControlPlaneRegistrationMessage message) { for (var auth : message.authorization()) { - if (!authorizationTypes.containsKey(auth.getType())) { + if (!authorizations.containsKey(auth.getType())) { return Result.failure(new AuthorizationNotSupported(auth)); } } @@ -371,12 +368,14 @@ public static class Builder { private final Dataplane dataplane = new Dataplane(); private Builder() { + } public Dataplane build() { if (dataplane.id == null) { dataplane.id = UUID.randomUUID().toString(); } + return dataplane; } @@ -430,8 +429,8 @@ public Builder onTerminate(OnTerminate onTerminate) { return this; } - public Builder registerAuthorization(String type, Class authorizationClass, BiConsumer authorizationFunction) { - dataplane.authorizationTypes.put(type, new AuthorizationType<>(type, authorizationClass, authorizationFunction)); + public Builder registerAuthorization(Authorization authorization) { + dataplane.authorizations.put(authorization.type(), authorization); return this; } } diff --git a/src/main/java/org/eclipse/dataplane/domain/controlplane/ControlPlane.java b/src/main/java/org/eclipse/dataplane/domain/controlplane/ControlPlane.java index 1509f35..15db0c0 100644 --- a/src/main/java/org/eclipse/dataplane/domain/controlplane/ControlPlane.java +++ b/src/main/java/org/eclipse/dataplane/domain/controlplane/ControlPlane.java @@ -15,8 +15,7 @@ package org.eclipse.dataplane.domain.controlplane; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import org.eclipse.dataplane.domain.registration.Authorization; -import org.eclipse.dataplane.domain.registration.RawAuthorization; +import org.eclipse.dataplane.domain.registration.AuthorizationProfile; import java.util.ArrayList; import java.util.List; @@ -26,7 +25,7 @@ public class ControlPlane { private String id; private String endpoint; - private final List authorizations = new ArrayList<>(); + private final List authorizations = new ArrayList<>(); public String getId() { return id; @@ -40,11 +39,11 @@ public static ControlPlane.Builder newInstance() { return new ControlPlane.Builder(); } - public List getAuthorizations() { + public List getAuthorizations() { return authorizations; } - public Authorization authorization() { + public AuthorizationProfile authorization() { return getAuthorizations().stream().findAny().orElse(null); } @@ -72,7 +71,7 @@ public Builder endpoint(String endpoint) { return this; } - public Builder authorization(List authorizations) { + public Builder authorization(List authorizations) { controlPlane.authorizations.addAll(authorizations); return this; } diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/Authorization.java b/src/main/java/org/eclipse/dataplane/domain/registration/Authorization.java index 6606f97..34f9cba 100644 --- a/src/main/java/org/eclipse/dataplane/domain/registration/Authorization.java +++ b/src/main/java/org/eclipse/dataplane/domain/registration/Authorization.java @@ -14,7 +14,22 @@ package org.eclipse.dataplane.domain.registration; +import java.net.http.HttpRequest; + +/** + * Defines structure for an authorization profile. + */ public interface Authorization { - String getType(); + /** + * Return the authorization profile type string + */ + String type(); + + /** + * Function that applies the authorization profile to the request builder. + * e.g. the Authorization header could be added with proper content. + */ + HttpRequest.Builder apply(HttpRequest.Builder requestBuilder, AuthorizationProfile profile); + } diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationProfile.java b/src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationProfile.java new file mode 100644 index 0000000..30eeeda --- /dev/null +++ b/src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationProfile.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Think-it GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Think-it GmbH - initial API and implementation + * + */ + +package org.eclipse.dataplane.domain.registration; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import org.eclipse.dataplane.port.exception.IllegalAttributeTypeException; + +import java.util.HashMap; +import java.util.Map; + +public class AuthorizationProfile { + + private final Map attributes; + + public AuthorizationProfile() { + attributes = new HashMap<>(); + } + + public AuthorizationProfile(String type) { + this(); + attributes.put("type", type); + } + + public String getType() { + return attributes.get("type").toString(); + } + + @JsonAnyGetter + public Map getAttributes() { + return attributes; + } + + @JsonAnySetter + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + public String stringAttribute(String key) { + var attribute = attributes.get(key); + if (attribute == null) { + return null; + } + + if (attribute instanceof String stringAttribute) { + return stringAttribute; + } + + throw new IllegalAttributeTypeException("Attribute %s is not a String but it's a %s".formatted(key, attribute.getClass().getSimpleName())); + } + + public AuthorizationProfile withAttribute(String key, String value) { + setAttribute(key, value); + return this; + } +} diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/ControlPlaneRegistrationMessage.java b/src/main/java/org/eclipse/dataplane/domain/registration/ControlPlaneRegistrationMessage.java index cd096f3..d8ce274 100644 --- a/src/main/java/org/eclipse/dataplane/domain/registration/ControlPlaneRegistrationMessage.java +++ b/src/main/java/org/eclipse/dataplane/domain/registration/ControlPlaneRegistrationMessage.java @@ -21,7 +21,7 @@ public record ControlPlaneRegistrationMessage( String controlplaneId, String endpoint, - List authorization + List authorization ) { public ControlPlaneRegistrationMessage(String controlplaneId, String endpoint) { this(controlplaneId, endpoint, emptyList()); diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java b/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java new file mode 100644 index 0000000..d5250d7 --- /dev/null +++ b/src/main/java/org/eclipse/dataplane/domain/registration/Oauth2ClientCredentialsAuthorization.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Think-it GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Think-it GmbH - initial API and implementation + * + */ + +package org.eclipse.dataplane.domain.registration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Collectors; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; + +public class Oauth2ClientCredentialsAuthorization implements Authorization { + + private final HttpClient httpClient = HttpClient.newHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String type() { + return "oauth2_client_credentials"; + } + + @Override + public HttpRequest.Builder apply(HttpRequest.Builder requestBuilder, AuthorizationProfile profile) { + var tokenEndpoint = profile.stringAttribute("tokenEndpoint"); + + var parameters = Map.of( + "grant_type", "client_credentials", + "client_id", profile.stringAttribute("clientId"), + "client_secret", profile.stringAttribute("clientSecret") + ); + + var form = parameters.entrySet() + .stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + + var request = HttpRequest.newBuilder(URI.create(tokenEndpoint)) + .POST(HttpRequest.BodyPublishers.ofString(form)) + .header("Content-Type", APPLICATION_FORM_URLENCODED) + .build(); + + try { + var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + var body = response.body(); + var accessToken = objectMapper.readValue(body, Map.class).get("access_token").toString(); + return requestBuilder.header(AUTHORIZATION, "Bearer " + accessToken); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } +} diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/RawAuthorization.java b/src/main/java/org/eclipse/dataplane/domain/registration/RawAuthorization.java deleted file mode 100644 index e069d69..0000000 --- a/src/main/java/org/eclipse/dataplane/domain/registration/RawAuthorization.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2026 Think-it GmbH - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Think-it GmbH - initial API and implementation - * - */ - -package org.eclipse.dataplane.domain.registration; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; - -import java.util.HashMap; -import java.util.Map; - -public class RawAuthorization implements Authorization { - - private final Map attributes = new HashMap<>(); - - @Override - public String getType() { - return attributes.get("type").toString(); - } - - @JsonAnyGetter - public Map getAttributes() { - return attributes; - } - - @JsonAnySetter - public void setAttribute(String key, Object value) { - attributes.put(key, value); - } -} diff --git a/src/main/java/org/eclipse/dataplane/port/exception/AuthorizationNotSupported.java b/src/main/java/org/eclipse/dataplane/port/exception/AuthorizationNotSupported.java index 4f91d9e..52a1110 100644 --- a/src/main/java/org/eclipse/dataplane/port/exception/AuthorizationNotSupported.java +++ b/src/main/java/org/eclipse/dataplane/port/exception/AuthorizationNotSupported.java @@ -14,12 +14,12 @@ package org.eclipse.dataplane.port.exception; -import org.eclipse.dataplane.domain.registration.Authorization; +import org.eclipse.dataplane.domain.registration.AuthorizationProfile; public class AuthorizationNotSupported extends Exception { - public AuthorizationNotSupported(Authorization authorization) { - super("Authorization type " + authorization.getType() + " not supported"); + public AuthorizationNotSupported(AuthorizationProfile authorizationProfile) { + super("Authorization type " + authorizationProfile.getType() + " not supported"); } } diff --git a/src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationType.java b/src/main/java/org/eclipse/dataplane/port/exception/IllegalAttributeTypeException.java similarity index 51% rename from src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationType.java rename to src/main/java/org/eclipse/dataplane/port/exception/IllegalAttributeTypeException.java index 09338ea..d30bd77 100644 --- a/src/main/java/org/eclipse/dataplane/domain/registration/AuthorizationType.java +++ b/src/main/java/org/eclipse/dataplane/port/exception/IllegalAttributeTypeException.java @@ -12,15 +12,10 @@ * */ -package org.eclipse.dataplane.domain.registration; - -import java.net.http.HttpRequest; -import java.util.function.BiConsumer; - -public record AuthorizationType( - String type, - Class authorizationClass, - BiConsumer authorizationFunction // TODO: dedicated interface -) { +package org.eclipse.dataplane.port.exception; +public class IllegalAttributeTypeException extends RuntimeException { + public IllegalAttributeTypeException(String message) { + super(message); + } } diff --git a/src/test/java/org/eclipse/dataplane/ControlPlane.java b/src/test/java/org/eclipse/dataplane/ControlPlane.java index 5d00fb2..8c383f4 100644 --- a/src/test/java/org/eclipse/dataplane/ControlPlane.java +++ b/src/test/java/org/eclipse/dataplane/ControlPlane.java @@ -16,11 +16,12 @@ import io.restassured.response.ValidatableResponse; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.Context; import org.eclipse.dataplane.domain.dataflow.DataFlowPrepareMessage; import org.eclipse.dataplane.domain.dataflow.DataFlowResponseMessage; import org.eclipse.dataplane.domain.dataflow.DataFlowStartMessage; @@ -28,7 +29,6 @@ import org.eclipse.dataplane.domain.dataflow.DataFlowSuspendMessage; import org.eclipse.dataplane.domain.dataflow.DataFlowTerminateMessage; -import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Predicate; @@ -43,26 +43,20 @@ */ public class ControlPlane { - private final DataplaneClient consumerClient; - private final DataplaneClient providerClient; - private final HttpServer httpServer; - private String authorizationToken; + private DataplaneClient consumerClient; + private DataplaneClient providerClient; + private HttpServer httpServer; + private Predicate authorizationValidation = c -> true; - public ControlPlane(HttpServer httpServer, String consumerDataPlanePath, String providerDataPlanePath) { + public void initialize(HttpServer httpServer, String consumerDataPlanePath, String providerDataPlanePath) { this.httpServer = httpServer; consumerClient = new DataplaneClient("http://localhost:%d%s".formatted(httpServer.port(), consumerDataPlanePath)); providerClient = new DataplaneClient("http://localhost:%d%s".formatted(httpServer.port(), providerDataPlanePath)); - Predicate authorizationProvider = authorization -> { - if (authorizationToken != null) { - return Objects.equals(authorization, authorizationToken); - } - return true; - }; + Predicate authorizationProvider = context -> authorizationValidation.test(context); httpServer.deploy("/consumer/control-plane", new ControlPlaneController(providerClient, authorizationProvider)); httpServer.deploy("/provider/control-plane", new ControlPlaneController(consumerClient, authorizationProvider)); - } public ValidatableResponse consumerPrepare(DataFlowPrepareMessage prepareMessage) { @@ -101,8 +95,13 @@ public String consumerCallbackAddress() { return "http://localhost:%d/consumer/control-plane".formatted(httpServer.port()); } + @Deprecated(forRemoval = true) public void setAuthorizationToken(String token) { - authorizationToken = token; + + } + + public void setAuthorizationValidation(Predicate authorizationValidation) { + this.authorizationValidation = authorizationValidation; } @Path("/transfers") @@ -110,18 +109,18 @@ public static class ControlPlaneController { private final ExecutorService executor = Executors.newCachedThreadPool(); private final DataplaneClient counterPart; - private final Predicate authorizationProvider; + private final Predicate authorizationValidation; - public ControlPlaneController(DataplaneClient counterPart, Predicate authorizationProvider) { + public ControlPlaneController(DataplaneClient counterPart, Predicate authorizationValidation) { this.counterPart = counterPart; - this.authorizationProvider = authorizationProvider; + this.authorizationValidation = authorizationValidation; } @POST @Path("/{transferId}/dataflow/prepared") @Consumes(WILDCARD) - public void prepared(@PathParam("transferId") String transferId, @HeaderParam("Authorization") String authorization, DataFlowResponseMessage message) { - if (!authorizationProvider.test(authorization)) { + public void prepared(@PathParam("transferId") String transferId, @Context ContainerRequestContext context, DataFlowResponseMessage message) { + if (!authorizationValidation.test(context)) { throw new NotAuthorizedException("Not authorized"); } assertThat(message.state()).isEqualTo("PREPARED"); @@ -130,8 +129,8 @@ public void prepared(@PathParam("transferId") String transferId, @HeaderParam("A @POST @Path("/{transferId}/dataflow/started") @Consumes(WILDCARD) - public void started(@PathParam("transferId") String transferId, @HeaderParam("Authorization") String authorization, DataFlowResponseMessage message) { - if (!authorizationProvider.test(authorization)) { + public void started(@PathParam("transferId") String transferId, @Context ContainerRequestContext context, DataFlowResponseMessage message) { + if (!authorizationValidation.test(context)) { throw new NotAuthorizedException("Not authorized"); } assertThat(message.state()).isEqualTo("STARTED"); @@ -140,8 +139,8 @@ public void started(@PathParam("transferId") String transferId, @HeaderParam("Au @POST @Path("/{transferId}/dataflow/completed") @Consumes(WILDCARD) - public void completed(@PathParam("transferId") String transferId, @HeaderParam("Authorization") String authorization) { - if (!authorizationProvider.test(authorization)) { + public void completed(@PathParam("transferId") String transferId, @Context ContainerRequestContext context) { + if (!authorizationValidation.test(context)) { throw new NotAuthorizedException("Not authorized"); } executor.submit(() -> { @@ -153,8 +152,8 @@ public void completed(@PathParam("transferId") String transferId, @HeaderParam(" @POST @Path("/{transferId}/dataflow/errored") @Consumes(WILDCARD) - public void errored(@PathParam("transferId") String transferId, @HeaderParam("Authorization") String authorization) { - if (!authorizationProvider.test(authorization)) { + public void errored(@PathParam("transferId") String transferId, @Context ContainerRequestContext context) { + if (!authorizationValidation.test(context)) { throw new NotAuthorizedException("Not authorized"); } executor.submit(() -> { diff --git a/src/test/java/org/eclipse/dataplane/DataplaneTest.java b/src/test/java/org/eclipse/dataplane/DataplaneTest.java index 6f79596..b6753f9 100644 --- a/src/test/java/org/eclipse/dataplane/DataplaneTest.java +++ b/src/test/java/org/eclipse/dataplane/DataplaneTest.java @@ -56,7 +56,6 @@ void tearDown() { controlPlane.stop(); } - @Nested class NotifyCompleted { diff --git a/src/test/java/org/eclipse/dataplane/HttpServer.java b/src/test/java/org/eclipse/dataplane/HttpServer.java index 5c0c575..4751e53 100644 --- a/src/test/java/org/eclipse/dataplane/HttpServer.java +++ b/src/test/java/org/eclipse/dataplane/HttpServer.java @@ -30,13 +30,12 @@ public class HttpServer { private final Server server; private final ServletContextHandler servletContextHandler = new ServletContextHandler(NO_SESSIONS); - private final int port; + private final ServerConnector connector; - public HttpServer(int port) { - this.port = port; + public HttpServer() { server = new Server(); - var connector = new ServerConnector(server); - connector.setPort(port); + connector = new ServerConnector(server); + connector.setPort(0); server.setConnectors(new Connector[]{connector}); server.setHandler(servletContextHandler); } @@ -78,6 +77,6 @@ protected void configure() { } public int port() { - return port; + return connector.getLocalPort(); } } diff --git a/src/test/java/org/eclipse/dataplane/api/ControlPlaneRegistrationApiTest.java b/src/test/java/org/eclipse/dataplane/api/ControlPlaneRegistrationApiTest.java index 7294392..817876c 100644 --- a/src/test/java/org/eclipse/dataplane/api/ControlPlaneRegistrationApiTest.java +++ b/src/test/java/org/eclipse/dataplane/api/ControlPlaneRegistrationApiTest.java @@ -14,18 +14,19 @@ package org.eclipse.dataplane.api; -import com.fasterxml.jackson.annotation.JsonProperty; import io.restassured.http.ContentType; import org.eclipse.dataplane.Dataplane; import org.eclipse.dataplane.HttpServer; +import org.eclipse.dataplane.domain.registration.Authorization; +import org.eclipse.dataplane.domain.registration.AuthorizationProfile; import org.eclipse.dataplane.domain.registration.ControlPlaneRegistrationMessage; -import org.eclipse.dataplane.domain.registration.RawAuthorization; import org.eclipse.dataplane.port.exception.ResourceNotFoundException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.net.http.HttpRequest; import java.util.List; import java.util.UUID; @@ -34,10 +35,10 @@ class ControlPlaneRegistrationApiTest { - private final HttpServer httpServer = new HttpServer(21361); + private final HttpServer httpServer = new HttpServer(); private final Dataplane sdk = Dataplane.newInstance() .id("consumer") - .registerAuthorization("test-authorization", TestAuthorization.class, (requestBuilder, authorization) -> {}) + .registerAuthorization(new TestAuthorization()) .build(); @BeforeEach @@ -111,13 +112,7 @@ void shouldReplaceControlPlane_whenSecondCall() { @Test void shouldReturnBadRequest_whenRequestedAuthMethodNotSupported() { var controlPlaneId = UUID.randomUUID().toString(); - var authorization = new RawAuthorization() { - - @Override - public String getType() { - return "unsupported"; - } - }; + var authorization = new AuthorizationProfile("unsupported"); var controlPlaneRegistrationMessage = new ControlPlaneRegistrationMessage(controlPlaneId, "http://something", List.of(authorization)); given() @@ -135,7 +130,7 @@ public String getType() { @Test void shouldRegisterAuthorizationType() { var controlPlaneId = UUID.randomUUID().toString(); - var authorization = new TestAuthorization("token"); + var authorization = new AuthorizationProfile("token"); var controlPlaneRegistrationMessage = new ControlPlaneRegistrationMessage(controlPlaneId, "http://something", List.of(authorization)); given() @@ -197,22 +192,17 @@ void shouldReturn404_whenControlPlaneDoesNotExist() { } } - private static class TestAuthorization extends RawAuthorization { - - @JsonProperty("type") private final String type = "test-authorization"; - @JsonProperty("token") private String token; - - TestAuthorization(String token) { - this.token = token; - } + private static class TestAuthorization implements Authorization { @Override - public String getType() { - return type; + public String type() { + return "token"; } - public String getToken() { - return token; + @Override + public HttpRequest.Builder apply(HttpRequest.Builder requestBuilder, AuthorizationProfile profile) { + return requestBuilder.header("Authorization", profile.stringAttribute("token")); } } + } diff --git a/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java b/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java new file mode 100644 index 0000000..d6625e2 --- /dev/null +++ b/src/test/java/org/eclipse/dataplane/scenario/AuthorizationOauth2Test.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2026 Think-it GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Think-it GmbH - initial API and implementation + * + */ + +package org.eclipse.dataplane.scenario; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import org.eclipse.dataplane.ControlPlane; +import org.eclipse.dataplane.Dataplane; +import org.eclipse.dataplane.HttpServer; +import org.eclipse.dataplane.domain.Result; +import org.eclipse.dataplane.domain.dataflow.DataFlowPrepareMessage; +import org.eclipse.dataplane.domain.dataflow.DataFlowResponseMessage; +import org.eclipse.dataplane.domain.registration.AuthorizationProfile; +import org.eclipse.dataplane.domain.registration.ControlPlaneRegistrationMessage; +import org.eclipse.dataplane.domain.registration.Oauth2ClientCredentialsAuthorization; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; + +public class AuthorizationOauth2Test { + + private final HttpServer httpServer = new HttpServer(); + private final ControlPlane controlPlane = new ControlPlane(); + private final Dataplane dataPlane = Dataplane.newInstance() + .id("data-plane") + .registerAuthorization(new Oauth2ClientCredentialsAuthorization()) + .onPrepare(dataFlow -> { + dataFlow.transitionToPreparing(); + return Result.success(dataFlow); + }) + .build(); + + private final String clientId = UUID.randomUUID().toString(); + private final String clientSecret = UUID.randomUUID().toString(); + + @BeforeEach + void setUp() { + httpServer.start(); + controlPlane.initialize(httpServer, "/data-plane", "/data-plane"); + + httpServer.deploy("/data-plane", dataPlane.controller()); + httpServer.deploy("/oauth2", new Oauth2TokenController(clientId, clientSecret)); + } + + @AfterEach + void tearDown() { + httpServer.stop(); + } + + @Test + void shouldCommunicateWithControlPlaneUsingOauth2Authorization() { + var controlplaneId = clientId; + + controlPlane.setAuthorizationValidation(requestContext -> requestContext + .containsHeaderString("Authorization", authorization -> isValidBearerTokenWithSub(authorization, controlplaneId))); + + var controlPlaneRegistrationMessage = new ControlPlaneRegistrationMessage( + controlplaneId, + controlPlane.consumerCallbackAddress(), + List.of(new AuthorizationProfile("oauth2_client_credentials") + .withAttribute("tokenEndpoint", "http://localhost:" + httpServer.port() + "/oauth2/token") + .withAttribute("clientId", clientId) + .withAttribute("clientSecret", clientSecret)) + ); + dataPlane.registerControlPlane(controlPlaneRegistrationMessage).orElseThrow(RuntimeException::new); + + var transferType = "FileSystemAsync-PUSH"; + var processId = UUID.randomUUID().toString(); + var consumerProcessId = "consumer_" + processId; + var prepareMessage = createPrepareMessage(consumerProcessId, controlPlane.consumerCallbackAddress(), transferType); + + controlPlane.consumerPrepare(prepareMessage).statusCode(202).extract().as(DataFlowResponseMessage.class); + + var notifyPreparedResult = dataPlane.getById(consumerProcessId) + .compose(dataFlow -> dataPlane.notifyPrepared(consumerProcessId, Result::success)); + + assertThat(notifyPreparedResult.succeeded()).isTrue(); + } + + private boolean isValidBearerTokenWithSub(String authorization, String controlplaneId) { + var bearer = "Bearer "; + var isBearer = authorization.startsWith(bearer); + if (!isBearer) { + return false; + } + + try { + var jwt = SignedJWT.parse(authorization.substring(bearer.length())); + var sub = jwt.getJWTClaimsSet().getClaims().get("sub"); + return sub.equals(controlplaneId); + } catch (ParseException e) { + return false; + } + } + + private @NonNull DataFlowPrepareMessage createPrepareMessage(String consumerProcessId, String callbackAddress, String transferType) { + return new DataFlowPrepareMessage("theMessageId", "theParticipantId", "theCounterPartyId", + "theDataspaceContext", consumerProcessId, "theAgreementId", "theDatasetId", callbackAddress, + transferType, emptyList(), emptyMap()); + } + + @Path("/") + public static class Oauth2TokenController { + + private final String clientId; + private final String clientSecret; + + public Oauth2TokenController(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + @POST + @Path("/token") + @Consumes(APPLICATION_FORM_URLENCODED) + @Produces(APPLICATION_JSON) + public Response token( + @FormParam("grant_type") String grantType, + @FormParam("client_id") String clientId, + @FormParam("client_secret") String clientSecret + ) { + if (!Objects.equals(clientId, this.clientId) || !Objects.equals(clientSecret, this.clientSecret) || !Objects.equals(grantType, "client_credentials")) { + return Response.status(401).build(); + } + + var token = issueJwt(clientId); + var responseBody = Map.of("access_token", token); + return Response.ok(responseBody).build(); + } + + public String issueJwt(String sub) { + var now = new Date(); + + var claimsSet = new JWTClaimsSet.Builder() + .subject(sub) + .issuer("https://your-app.com") + .audience("https://api.your-app.com") + .expirationTime(new Date(now.getTime() + 1000)) + .notBeforeTime(now) + .issueTime(now) + .jwtID(UUID.randomUUID().toString()) + .build(); + + var header = new JWSHeader.Builder(JWSAlgorithm.HS256) + .type(JOSEObjectType.JWT) + .build(); + + var signedJwt = new SignedJWT(header, claimsSet); + + var secret = "random-256-bit-secret-" + UUID.randomUUID(); + try { + var signer = new MACSigner(secret.getBytes()); + signedJwt.sign(signer); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + + return signedJwt.serialize(); + } + + } + +} diff --git a/src/test/java/org/eclipse/dataplane/scenario/AuthorizationTest.java b/src/test/java/org/eclipse/dataplane/scenario/AuthorizationTest.java index 51794d5..64aad44 100644 --- a/src/test/java/org/eclipse/dataplane/scenario/AuthorizationTest.java +++ b/src/test/java/org/eclipse/dataplane/scenario/AuthorizationTest.java @@ -20,12 +20,15 @@ import org.eclipse.dataplane.domain.Result; import org.eclipse.dataplane.domain.dataflow.DataFlowPrepareMessage; import org.eclipse.dataplane.domain.dataflow.DataFlowResponseMessage; +import org.eclipse.dataplane.domain.registration.Authorization; +import org.eclipse.dataplane.domain.registration.AuthorizationProfile; import org.eclipse.dataplane.domain.registration.ControlPlaneRegistrationMessage; -import org.eclipse.dataplane.domain.registration.RawAuthorization; import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.net.http.HttpRequest; import java.util.List; import java.util.UUID; @@ -35,12 +38,11 @@ public class AuthorizationTest { - private final HttpServer httpServer = new HttpServer(21961); - private final ControlPlane controlPlane = new ControlPlane(httpServer, "/data-plane", "/data-plane"); + private final HttpServer httpServer = new HttpServer(); + private final ControlPlane controlPlane = new ControlPlane(); private final Dataplane dataPlane = Dataplane.newInstance() .id("data-plane") - .registerAuthorization("test-authorization", TestAuthorization.class, - (builder, authorization) -> builder.header("Authorization", authorization.getToken())) + .registerAuthorization(new TestAuthorization()) .onPrepare(dataFlow -> { dataFlow.transitionToPreparing(); return Result.success(dataFlow); @@ -50,18 +52,27 @@ public class AuthorizationTest { @BeforeEach void setUp() { httpServer.start(); + controlPlane.initialize(httpServer, "/data-plane", "/data-plane"); httpServer.deploy("/data-plane", dataPlane.controller()); } + @AfterEach + void tearDown() { + httpServer.stop(); + } + @Test void shouldCommunicateWithControlPlaneUsingRegisteredAuthorization() { var authorizationToken = UUID.randomUUID().toString(); - controlPlane.setAuthorizationToken(authorizationToken); + controlPlane.setAuthorizationValidation(requestContext -> + requestContext.containsHeaderString("Authorization", a -> a.equals(authorizationToken))); + var authorizationProfile = new AuthorizationProfile("test-authorization"); + authorizationProfile.setAttribute("token", authorizationToken); var controlPlaneRegistrationMessage = new ControlPlaneRegistrationMessage( UUID.randomUUID().toString(), controlPlane.consumerCallbackAddress(), - List.of(new TestAuthorization(authorizationToken)) + List.of(authorizationProfile) ); dataPlane.registerControlPlane(controlPlaneRegistrationMessage).orElseThrow(RuntimeException::new); @@ -84,28 +95,17 @@ void shouldCommunicateWithControlPlaneUsingRegisteredAuthorization() { transferType, emptyList(), emptyMap()); } - private static class TestAuthorization extends RawAuthorization { - - private final String type; - private String token; - - TestAuthorization() { - type = "test-authorization"; - } - - TestAuthorization(String token) { - this(); - this.token = token; - } + private static class TestAuthorization implements Authorization { @Override - public String getType() { - return type; + public String type() { + return "test-authorization"; } - public String getToken() { - return token; + @Override + public HttpRequest.Builder apply(HttpRequest.Builder requestBuilder, AuthorizationProfile profile) { + return requestBuilder.header("Authorization", profile.stringAttribute("token")); } - } + } diff --git a/src/test/java/org/eclipse/dataplane/scenario/ConsumerPullTest.java b/src/test/java/org/eclipse/dataplane/scenario/ConsumerPullTest.java index 49db959..0559b94 100644 --- a/src/test/java/org/eclipse/dataplane/scenario/ConsumerPullTest.java +++ b/src/test/java/org/eclipse/dataplane/scenario/ConsumerPullTest.java @@ -47,16 +47,17 @@ class ConsumerPullTest { - private final HttpServer httpServer = new HttpServer(21341); + private final HttpServer httpServer = new HttpServer(); private final int filesAvailableOnProvider = 13; - private final ControlPlane controlPlane = new ControlPlane(httpServer, "/consumer/data-plane", "/provider/data-plane"); + private final ControlPlane controlPlane = new ControlPlane(); private final ConsumerDataPlane consumerDataPlane = new ConsumerDataPlane(); private final ProviderDataPlane providerDataPlane = new ProviderDataPlane(filesAvailableOnProvider); @BeforeEach void setUp() { httpServer.start(); + controlPlane.initialize(httpServer, "/consumer/data-plane", "/provider/data-plane"); httpServer.deploy("/consumer/data-plane", consumerDataPlane.controller()); httpServer.deploy("/provider/data-plane", providerDataPlane.controller()); diff --git a/src/test/java/org/eclipse/dataplane/scenario/ProviderPushTest.java b/src/test/java/org/eclipse/dataplane/scenario/ProviderPushTest.java index c4a0f58..b3728b7 100644 --- a/src/test/java/org/eclipse/dataplane/scenario/ProviderPushTest.java +++ b/src/test/java/org/eclipse/dataplane/scenario/ProviderPushTest.java @@ -49,15 +49,16 @@ public class ProviderPushTest { - private final HttpServer httpServer = new HttpServer(21341); + private final HttpServer httpServer = new HttpServer(); - private final ControlPlane controlPlane = new ControlPlane(httpServer, "/consumer/data-plane", "/provider/data-plane"); + private final ControlPlane controlPlane = new ControlPlane(); private final ConsumerDataPlane consumerDataPlane = new ConsumerDataPlane(); private final ProviderDataPlane providerDataPlane = new ProviderDataPlane(); @BeforeEach void setUp() { httpServer.start(); + controlPlane.initialize(httpServer, "/consumer/data-plane", "/provider/data-plane"); httpServer.deploy("/consumer/data-plane", consumerDataPlane.controller()); httpServer.deploy("/provider/data-plane", providerDataPlane.controller()); diff --git a/src/test/java/org/eclipse/dataplane/scenario/StreamingPullTest.java b/src/test/java/org/eclipse/dataplane/scenario/StreamingPullTest.java index c5d3d6f..ee210a3 100644 --- a/src/test/java/org/eclipse/dataplane/scenario/StreamingPullTest.java +++ b/src/test/java/org/eclipse/dataplane/scenario/StreamingPullTest.java @@ -52,15 +52,16 @@ class StreamingPullTest { - private final HttpServer httpServer = new HttpServer(21341); + private final HttpServer httpServer = new HttpServer(); - private final ControlPlane controlPlane = new ControlPlane(httpServer, "/consumer/data-plane", "/provider/data-plane"); + private final ControlPlane controlPlane = new ControlPlane(); private final ConsumerDataPlane consumerDataPlane = new ConsumerDataPlane(); private final ProviderDataPlane providerDataPlane = new ProviderDataPlane(); @BeforeEach void setUp() { httpServer.start(); + controlPlane.initialize(httpServer, "/consumer/data-plane", "/provider/data-plane"); httpServer.deploy("/consumer/data-plane", consumerDataPlane.controller()); httpServer.deploy("/provider/data-plane", providerDataPlane.controller()); diff --git a/src/test/java/org/eclipse/dataplane/scenario/StreamingPushTest.java b/src/test/java/org/eclipse/dataplane/scenario/StreamingPushTest.java index 90974fd..285ec4d 100644 --- a/src/test/java/org/eclipse/dataplane/scenario/StreamingPushTest.java +++ b/src/test/java/org/eclipse/dataplane/scenario/StreamingPushTest.java @@ -48,15 +48,16 @@ public class StreamingPushTest { - private final HttpServer httpServer = new HttpServer(21341); + private final HttpServer httpServer = new HttpServer(); - private final ControlPlane controlPlane = new ControlPlane(httpServer, "/consumer/data-plane", "/provider/data-plane"); + private final ControlPlane controlPlane = new ControlPlane(); private final ConsumerDataPlane consumerDataPlane = new ConsumerDataPlane(); private final ProviderDataPlane providerDataPlane = new ProviderDataPlane(); @BeforeEach void setUp() { httpServer.start(); + controlPlane.initialize(httpServer, "/consumer/data-plane", "/provider/data-plane"); httpServer.deploy("/consumer/data-plane", consumerDataPlane.controller()); httpServer.deploy("/provider/data-plane", providerDataPlane.controller());