diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/build.gradle.kts b/edc-dataplane/edc-dataplane-proxy-consumer-api/build.gradle.kts new file mode 100644 index 000000000..6af73c993 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + + implementation(libs.jakarta.rsApi) + + implementation(libs.edc.spi.http) + implementation(libs.edc.util) + implementation(libs.edc.dpf.framework) + implementation(libs.edc.api.observability) + implementation(libs.edc.dpf.util) + implementation(libs.edc.ext.http) + + implementation(project(":spi:edr-cache-spi")) + + testImplementation(libs.edc.junit) +} + diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/DataPlaneProxyConsumerApiExtension.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/DataPlaneProxyConsumerApiExtension.java new file mode 100644 index 000000000..566a63b7b --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/DataPlaneProxyConsumerApiExtension.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api; + +import org.eclipse.edc.connector.dataplane.spi.manager.DataPlaneManager; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebServer; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer; +import org.eclipse.edc.web.spi.configuration.WebServiceSettings; +import org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.ConsumerAssetRequestController; +import org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.ClientErrorExceptionMapper; +import org.eclipse.tractusx.edc.edr.spi.EndpointDataReferenceCache; + +import java.util.concurrent.ExecutorService; + +import static java.util.concurrent.Executors.newFixedThreadPool; + +/** + * Instantiates the Proxy Data API for the consumer-side data plane. + */ +@Extension(value = DataPlaneProxyConsumerApiExtension.NAME) +public class DataPlaneProxyConsumerApiExtension implements ServiceExtension { + static final String NAME = "Data Plane Proxy Consumer API"; + + private static final int DEFAULT_PROXY_PORT = 8186; + private static final String CONSUMER_API_ALIAS = "consumer.api"; + private static final String CONSUMER_CONTEXT_PATH = "/proxy"; + private static final String CONSUMER_CONFIG_KEY = "web.http.proxy"; + + @Setting(value = "Data plane proxy API consumer port", type = "int") + private static final String CONSUMER_PORT = "tx.dpf.consumer.proxy.port"; + + @Setting(value = "Thread pool size for the consumer data plane proxy gateway", type = "int") + private static final String THREAD_POOL_SIZE = "tx.dpf.consumer.proxy.thread.pool"; + + public static final int DEFAULT_THREAD_POOL = 10; + + @Inject + private WebService webService; + + @Inject + private WebServer webServer; + + @Inject + private DataPlaneManager dataPlaneManager; + + @Inject + private EndpointDataReferenceCache edrCache; + + @Inject + private WebServiceConfigurer configurer; + + @Inject + private Monitor monitor; + + private ExecutorService executorService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var port = context.getSetting(CONSUMER_PORT, DEFAULT_PROXY_PORT); + configurer.configure(context, webServer, createApiContext(port)); + + executorService = newFixedThreadPool(context.getSetting(THREAD_POOL_SIZE, DEFAULT_THREAD_POOL)); + + webService.registerResource(CONSUMER_API_ALIAS, new ClientErrorExceptionMapper()); + webService.registerResource(CONSUMER_API_ALIAS, new ConsumerAssetRequestController(edrCache, dataPlaneManager, executorService, monitor)); + } + + @Override + public void shutdown() { + if (executorService != null) { + executorService.shutdown(); + } + } + + private WebServiceSettings createApiContext(int port) { + return WebServiceSettings.Builder.newInstance() + .apiConfigKey(CONSUMER_CONFIG_KEY) + .contextAlias(CONSUMER_API_ALIAS) + .defaultPath(CONSUMER_CONTEXT_PATH) + .defaultPort(port) + .name(NAME) + .build(); + } + +} diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ClientErrorExceptionMapper.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ClientErrorExceptionMapper.java new file mode 100644 index 000000000..386428084 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ClientErrorExceptionMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Maps client errors to return the associated status. + */ +@Provider +public class ClientErrorExceptionMapper implements ExceptionMapper { + + public ClientErrorExceptionMapper() { + } + + @Override + public Response toResponse(ClientErrorException exception) { + return Response.status(exception.getResponse().getStatus()).build(); + } +} + + diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestApi.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestApi.java new file mode 100644 index 000000000..7902566aa --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestApi.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.model.AssetRequest; + +/** + * Defines the API for receiving asset requests on a consumer. + */ +@OpenAPIDefinition +@Tag(name = "Data Plane Proxy API") +public interface ConsumerAssetRequestApi { + + @Operation(responses = { + @ApiResponse(content = @Content(mediaType = "application/json", schema = @Schema(implementation = AssetRequest.class)), description = "Requests asset data") + }) + void requestAsset(AssetRequest request, @Suspended AsyncResponse response); +} diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestController.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestController.java new file mode 100644 index 000000000..2ceb4a393 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/ConsumerAssetRequestController.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.StreamingOutput; +import org.eclipse.edc.connector.dataplane.spi.manager.DataPlaneManager; +import org.eclipse.edc.connector.dataplane.spi.pipeline.StreamResult; +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.edr.EndpointDataReference; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowRequest; +import org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.model.AssetRequest; +import org.eclipse.tractusx.edc.edr.spi.EndpointDataReferenceCache; + +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.BAD_GATEWAY; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; +import static jakarta.ws.rs.core.Response.status; +import static java.lang.String.format; +import static java.util.UUID.randomUUID; + +/** + * Implements the HTTP proxy API. + */ +@Path("/aas") +public class ConsumerAssetRequestController implements ConsumerAssetRequestApi { + private static final String HTTP_DATA = "HttpData"; + private static final String ASYNC_TYPE = "async"; + private static final String BASE_URL = "baseUrl"; + private static final String HEADER_AUTHORIZATION = "header:authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private final EndpointDataReferenceCache edrCache; + private final DataPlaneManager dataPlaneManager; + private final Monitor monitor; + + private final ExecutorService executorService; + + public ConsumerAssetRequestController(EndpointDataReferenceCache edrCache, + DataPlaneManager dataPlaneManager, + ExecutorService executorService, + Monitor monitor) { + this.edrCache = edrCache; + this.dataPlaneManager = dataPlaneManager; + this.executorService = executorService; + this.monitor = monitor; + } + + @POST + @Path("/request") + @Override + public void requestAsset(AssetRequest request, @Suspended AsyncResponse response) { + // resolve the EDR and add it to the request + var edr = resolveEdr(request); + + var sourceAddress = DataAddress.Builder.newInstance() + .type(HTTP_DATA) + .property(BASE_URL, request.getEndpointUrl()) + .property(HEADER_AUTHORIZATION, BEARER_PREFIX + edr.getAuthCode()) + .build(); + + var destinationAddress = DataAddress.Builder.newInstance() + .type(ASYNC_TYPE) + .build(); + + var flowRequest = DataFlowRequest.Builder.newInstance() + .processId(randomUUID().toString()) + .trackable(false) + .sourceDataAddress(sourceAddress) + .destinationDataAddress(destinationAddress) + .traceContext(Map.of()) + .build(); + + // transfer the data asynchronously + var sink = new AsyncStreamingDataSink(consumer -> response.resume((StreamingOutput) consumer::accept), executorService, monitor); + + try { + dataPlaneManager.transfer(sink, flowRequest).whenComplete((result, throwable) -> handleCompletion(response, result, throwable)); + } catch (Exception e) { + reportError(response, e); + } + } + + private EndpointDataReference resolveEdr(AssetRequest request) { + if (request.getTransferProcessId() != null) { + var edr = edrCache.resolveReference(request.getTransferProcessId()); + if (edr == null) { + throw new BadRequestException("No EDR for transfer process: " + request.getTransferProcessId()); + } + return edr; + } else { + var resolvedEdrs = edrCache.referencesForAsset(request.getAssetId()); + if (resolvedEdrs.isEmpty()) { + throw new BadRequestException("No EDR for asset: " + request.getAssetId()); + } else if (resolvedEdrs.size() > 1) { + throw new PreconditionFailedException("More than one EDR for asset: " + request.getAssetId()); + } + return resolvedEdrs.get(0); + } + } + + /** + * Handles a request completion, checking for errors. If no errors are present, nothing needs to be done as the response will have already been written to the client. + */ + private void handleCompletion(AsyncResponse response, StreamResult result, Throwable throwable) { + if (result != null && result.failed()) { + switch (result.reason()) { + case NOT_FOUND: + response.resume(status(NOT_FOUND).type(APPLICATION_JSON).build()); + break; + case NOT_AUTHORIZED: + response.resume(status(UNAUTHORIZED).type(APPLICATION_JSON).build()); + break; + case GENERAL_ERROR: + response.resume(status(INTERNAL_SERVER_ERROR).type(APPLICATION_JSON).build()); + break; + } + } else if (throwable != null) { + reportError(response, throwable); + } + } + + /** + * Reports an error to the client. On the consumer side, the error is reported as a {@code BAD_GATEWAY} since the consumer data plane acts as proxy. + */ + private void reportError(AsyncResponse response, Throwable throwable) { + monitor.severe("Error processing gateway request", throwable); + var entity = status(BAD_GATEWAY).entity(format("'%s'", throwable.getMessage())).type(APPLICATION_JSON).build(); + response.resume(entity); + } + +} diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/PreconditionFailedException.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/PreconditionFailedException.java new file mode 100644 index 000000000..24b865bf0 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/PreconditionFailedException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset; + +import jakarta.ws.rs.ClientErrorException; + +import static jakarta.ws.rs.core.Response.Status.PRECONDITION_REQUIRED; + +/** + * Exception used to map a {@link jakarta.ws.rs.core.Response.Status#PRECONDITION_REQUIRED} response. + */ +public class PreconditionFailedException extends ClientErrorException { + + public PreconditionFailedException(String message) { + super(message, PRECONDITION_REQUIRED); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequest.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequest.java new file mode 100644 index 000000000..79fbf70dd --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import static java.util.Objects.requireNonNull; + +/** + * A request for asset data. The request may contain a transfer process ID or asset ID and must specify an endpoint for retrieving the data. + */ +@JsonDeserialize(builder = AssetRequest.Builder.class) +@JsonTypeName("tx:assetrequest") +public class AssetRequest { + private String transferProcessId; + private String assetId; + private String endpointUrl; + + public String getTransferProcessId() { + return transferProcessId; + } + + public String getAssetId() { + return assetId; + } + + public String getEndpointUrl() { + return endpointUrl; + } + + private AssetRequest() { + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder { + private final AssetRequest request; + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder transferProcessId(String transferProcessId) { + request.transferProcessId = transferProcessId; + return this; + } + + public Builder assetId(String assetId) { + request.assetId = assetId; + return this; + } + + public Builder endpointUrl(String endpointUrl) { + request.endpointUrl = endpointUrl; + return this; + } + + public AssetRequest build() { + if (request.assetId == null && request.transferProcessId == null) { + throw new NullPointerException("An assetId or endpointReferenceId must be set"); + } + requireNonNull(request.endpointUrl, "endpointUrl"); + return request; + } + + private Builder() { + request = new AssetRequest(); + } + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..11229c1f5 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,13 @@ + # + # Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + # + # 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: + # Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + + org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.DataPlaneProxyConsumerApiExtension diff --git a/edc-dataplane/edc-dataplane-proxy-consumer-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequestTest.java b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequestTest.java new file mode 100644 index 000000000..5855b20dd --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-consumer-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/consumer/api/asset/model/AssetRequestTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.consumer.api.asset.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AssetRequestTest { + + @Test + void verify_SerializeDeserialize() throws JsonProcessingException { + var mapper = new ObjectMapper(); + + var request = AssetRequest.Builder.newInstance().assetId("asset1").endpointUrl("https://test.com").transferProcessId("tp1").build(); + var serialized = mapper.writeValueAsString(request); + + var deserialized = mapper.readValue(serialized, AssetRequest.class); + + assertThat(deserialized.getAssetId()).isEqualTo(request.getAssetId()); + assertThat(deserialized.getTransferProcessId()).isEqualTo(request.getTransferProcessId()); + assertThat(deserialized.getEndpointUrl()).isEqualTo(request.getEndpointUrl()); + } + + @Test + void verify_NullArguments() { + assertThatThrownBy(() -> AssetRequest.Builder.newInstance().endpointUrl("https://test.com").build()).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> AssetRequest.Builder.newInstance().assetId("asset1").build()).isInstanceOf(NullPointerException.class); + } + + @Test + void verify_AssetIdOrTransferProcessId() { + AssetRequest.Builder.newInstance().assetId("asset1").endpointUrl("https://test.com").build(); + AssetRequest.Builder.newInstance().transferProcessId("tp1").endpointUrl("https://test.com").build(); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/build.gradle.kts b/edc-dataplane/edc-dataplane-proxy-provider-api/build.gradle.kts new file mode 100644 index 000000000..37aabac01 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + + implementation(libs.edc.spi.http) + implementation(libs.edc.util) + implementation(libs.edc.dpf.framework) + implementation(libs.edc.api.observability) + implementation(libs.edc.dpf.util) + implementation(libs.edc.ext.http) + implementation(libs.edc.spi.jwt) + implementation(libs.edc.jwt.core) + + implementation(libs.jakarta.rsApi) + implementation(libs.nimbus.jwt) + + implementation(project(":edc-dataplane:edc-dataplane-proxy-provider-spi")) +} + diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/DataPlaneProxyProviderApiExtension.java b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/DataPlaneProxyProviderApiExtension.java new file mode 100644 index 000000000..6e845090c --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/DataPlaneProxyProviderApiExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.api; + +import org.eclipse.edc.connector.dataplane.spi.manager.DataPlaneManager; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.tractusx.edc.dataplane.proxy.provider.api.gateway.ProviderGatewayController; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandlerRegistry; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfigurationRegistry; + +import java.util.concurrent.ExecutorService; + +import static java.util.concurrent.Executors.newFixedThreadPool; + +/** + * Adds the consumer proxy data plane API. + */ +@Extension(value = DataPlaneProxyProviderApiExtension.NAME) +public class DataPlaneProxyProviderApiExtension implements ServiceExtension { + static final String NAME = "Data Plane Proxy Provider API"; + + @Setting(value = "Thread pool size for the provider data plane proxy gateway", type = "int") + private static final String THREAD_POOL_SIZE = "tx.dpf.provider.proxy.thread.pool"; + + public static final int DEFAULT_THREAD_POOL = 10; + + @Inject + private WebService webService; + + @Inject + private DataPlaneManager dataPlaneManager; + + @Inject + private Monitor monitor; + + @Inject + private GatewayConfigurationRegistry configurationRegistry; + + @Inject + private AuthorizationHandlerRegistry authorizationRegistry; + + private ExecutorService executorService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + executorService = newFixedThreadPool(context.getSetting(THREAD_POOL_SIZE, DEFAULT_THREAD_POOL)); + + var controller = new ProviderGatewayController(dataPlaneManager, + configurationRegistry, + authorizationRegistry, + executorService, + monitor); + + webService.registerResource(controller); + } + + + @Override + public void shutdown() { + if (executorService != null) { + executorService.shutdown(); + } + } + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayApi.java b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayApi.java new file mode 100644 index 000000000..f951123b3 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayApi.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.api.gateway; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Context; + +/** + * Open API definition. + */ +@OpenAPIDefinition +@Tag(name = "Data Plane Proxy API") +public interface ProviderGatewayApi { + + @Operation(responses = { + @ApiResponse(content = @Content(mediaType = "application/json"), description = "Gets asset data") + }) + void requestAsset(@Context ContainerRequestContext context, @Suspended AsyncResponse response); +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayController.java b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayController.java new file mode 100644 index 000000000..2f392fdce --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/gateway/ProviderGatewayController.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.api.gateway; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.PathSegment; +import jakarta.ws.rs.core.StreamingOutput; +import org.eclipse.edc.connector.dataplane.spi.manager.DataPlaneManager; +import org.eclipse.edc.connector.dataplane.spi.pipeline.StreamResult; +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowRequest; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandlerRegistry; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfigurationRegistry; + +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; +import static jakarta.ws.rs.core.Response.status; +import static java.lang.String.format; +import static java.util.UUID.randomUUID; +import static java.util.stream.Collectors.joining; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.api.response.ResponseHelper.createMessageResponse; + +/** + * Implements the HTTP data proxy API. + */ +@Path("/" + ProviderGatewayController.GATEWAY_PATH) +public class ProviderGatewayController implements ProviderGatewayApi{ + protected static final String GATEWAY_PATH = "gateway"; + + private static final String HTTP_DATA = "HttpData"; + private static final String BASE_URL = "baseUrl"; + private static final String ASYNC = "async"; + + private static final int ALIAS_SEGMENT = 1; + private static final String BEARER_PREFIX = "Bearer "; + + private final DataPlaneManager dataPlaneManager; + private final GatewayConfigurationRegistry configurationRegistry; + private final AuthorizationHandlerRegistry authorizationRegistry; + + private final Monitor monitor; + + private final ExecutorService executorService; + + public ProviderGatewayController(DataPlaneManager dataPlaneManager, + GatewayConfigurationRegistry configurationRegistry, + AuthorizationHandlerRegistry authorizationRegistry, + ExecutorService executorService, + Monitor monitor) { + this.dataPlaneManager = dataPlaneManager; + this.configurationRegistry = configurationRegistry; + this.authorizationRegistry = authorizationRegistry; + this.executorService = executorService; + this.monitor = monitor; + } + + @GET + @Path("/{paths: .+}") + @Override + public void requestAsset(@Context ContainerRequestContext context, @Suspended AsyncResponse response) { + var tokens = context.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (tokens == null || tokens.isEmpty()) { + response.resume(createMessageResponse(UNAUTHORIZED, "No bearer token", context.getMediaType())); + return; + } + var token = tokens.get(0); + if (!token.startsWith(BEARER_PREFIX)) { + response.resume(createMessageResponse(UNAUTHORIZED, "Invalid bearer token", context.getMediaType())); + return; + } else { + token = token.substring(BEARER_PREFIX.length()); + } + + var uriInfo = context.getUriInfo(); + var segments = uriInfo.getPathSegments(); + if (segments.size() < 3 || !GATEWAY_PATH.equals(segments.get(0).getPath())) { + response.resume(createMessageResponse(BAD_REQUEST, "Invalid path", context.getMediaType())); + return; + } + + var alias = segments.get(ALIAS_SEGMENT).getPath(); + var configuration = configurationRegistry.getConfiguration(alias); + if (configuration == null) { + response.resume(createMessageResponse(NOT_FOUND, "Unknown path", context.getMediaType())); + return; + } + + // calculate the sub-path, which all segments after the GATEWAY segment, including the alias segment + var subPath = segments.stream().skip(1).map(PathSegment::getPath).collect(joining("/")); + if (!authenticate(token, configuration.getAuthorizationType(), subPath, context, response)) { + return; + } + + // calculate the request path, which all segments after the alias segment + var requestPath = segments.stream().skip(2).map(PathSegment::getPath).collect(joining("/")); + var flowRequest = createRequest(requestPath, configuration); + + // transfer the data asynchronously + var sink = new AsyncStreamingDataSink(consumer -> response.resume((StreamingOutput) consumer::accept), executorService, monitor); + + try { + dataPlaneManager.transfer(sink, flowRequest).whenComplete((result, throwable) -> handleCompletion(response, result, throwable)); + } catch (Exception e) { + reportError(response, e); + } + } + + private DataFlowRequest createRequest(String subPath, GatewayConfiguration configuration) { + var path = configuration.getProxiedPath() + "/" + subPath; + + var sourceAddress = DataAddress.Builder.newInstance() + .type(HTTP_DATA) + .property(BASE_URL, path) + .build(); + + var destinationAddress = DataAddress.Builder.newInstance() + .type(ASYNC) + .build(); + + return DataFlowRequest.Builder.newInstance() + .processId(randomUUID().toString()) + .trackable(false) + .sourceDataAddress(sourceAddress) + .destinationDataAddress(destinationAddress) + .traceContext(Map.of()) + .build(); + } + + private boolean authenticate(String token, String authType, String subPath, ContainerRequestContext context, AsyncResponse response) { + var handler = authorizationRegistry.getHandler(authType); + if (handler == null) { + var correlationId = randomUUID().toString(); + monitor.severe(format("Authentication handler not configured for type: %s [id: %s]", authType, correlationId)); + response.resume(createMessageResponse(INTERNAL_SERVER_ERROR, format("Internal server error: %s", correlationId), context.getMediaType())); + return false; + } + + var authResponse = handler.authorize(token, subPath); + if (authResponse.failed()) { + response.resume(status(UNAUTHORIZED).build()); + return false; + } + return true; + } + + /** + * Handles a request completion, checking for errors. If no errors are present, nothing needs to be done as the response will have already been written to the client. + */ + private void handleCompletion(AsyncResponse response, StreamResult result, Throwable throwable) { + if (result != null && result.failed()) { + switch (result.reason()) { + case NOT_FOUND: + response.resume(status(NOT_FOUND).type(APPLICATION_JSON).build()); + break; + case NOT_AUTHORIZED: + response.resume(status(UNAUTHORIZED).type(APPLICATION_JSON).build()); + break; + case GENERAL_ERROR: + response.resume(status(INTERNAL_SERVER_ERROR).type(APPLICATION_JSON).build()); + break; + } + } else if (throwable != null) { + reportError(response, throwable); + } + } + + /** + * Reports an error to the client. On the provider side, the error is reported as a {@code INTERNAL_SERVER_ERROR} since the provider data plane is considered an origin server + * even though it may delegate requests to other internal sources. + */ + private void reportError(AsyncResponse response, Throwable throwable) { + monitor.severe("Error processing gateway request", throwable); + var entity = status(INTERNAL_SERVER_ERROR).entity(format("'%s'", throwable.getMessage())).type(APPLICATION_JSON).build(); + response.resume(entity); + } + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelper.java b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelper.java new file mode 100644 index 000000000..816c7a446 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelper.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.api.response; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jetbrains.annotations.Nullable; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; +import static jakarta.ws.rs.core.Response.status; +import static java.lang.String.format; + +/** + * Utility functions for creating responses. + */ +public class ResponseHelper { + + /** + * Creates a response with a message encoded for the given media type. Currently, {@code APPLICATION_JSON} and {@code TEXT_PLAIN} are supported. + */ + public static Response createMessageResponse(Response.Status status, String message, @Nullable MediaType mediaType) { + if (mediaType != null && APPLICATION_JSON.equals(mediaType.toString())) { + return status(status).entity(format("'%s'", message)).type(APPLICATION_JSON).build(); + } else { + return status(status).entity(format("%s", message)).type(TEXT_PLAIN).build(); + } + } + + private ResponseHelper() { + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..be66cb9c7 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,13 @@ + # + # Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + # + # 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: + # Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + +org.eclipse.tractusx.edc.dataplane.proxy.provider.api.DataPlaneProxyProviderApiExtension diff --git a/edc-dataplane/edc-dataplane-proxy-provider-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelperTest.java b/edc-dataplane/edc-dataplane-proxy-provider-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelperTest.java new file mode 100644 index 000000000..f9abd0327 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-api/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/api/response/ResponseHelperTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.api.response; + +import org.junit.jupiter.api.Test; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN_TYPE; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.api.response.ResponseHelper.createMessageResponse; + +class ResponseHelperTest { + + @Test + void verify_responses() { + assertThat(createMessageResponse(INTERNAL_SERVER_ERROR, "Some error", APPLICATION_JSON_TYPE).getEntity()).isEqualTo("'Some error'"); + assertThat(createMessageResponse(INTERNAL_SERVER_ERROR, "Some error", TEXT_PLAIN_TYPE).getEntity()).isEqualTo("Some error"); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/build.gradle.kts b/edc-dataplane/edc-dataplane-proxy-provider-core/build.gradle.kts new file mode 100644 index 000000000..fae93ad79 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + + implementation(libs.edc.util) + implementation(libs.edc.dpf.framework) + implementation(libs.edc.api.observability) + implementation(libs.edc.dpf.util) + implementation(libs.edc.jwt.core) + implementation(libs.edc.ext.http) + implementation(libs.edc.spi.http) + + implementation(libs.edc.spi.jwt) + + implementation(libs.jakarta.rsApi) + implementation(libs.nimbus.jwt) + + implementation(project(":edc-dataplane:edc-dataplane-proxy-provider-spi")) +} + diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/ProxyProviderCoreExtension.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/ProxyProviderCoreExtension.java new file mode 100644 index 000000000..5f8dd66e2 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/ProxyProviderCoreExtension.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core; + +import com.nimbusds.jose.crypto.RSASSAVerifier; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth.AuthorizationHandlerRegistryImpl; +import org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth.JwtAuthorizationHandler; +import org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth.RsaPublicKeyParser; +import org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration.GatewayConfigurationRegistryImpl; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationExtension; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandler; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandlerRegistry; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfigurationRegistry; +import org.jetbrains.annotations.NotNull; + +import static java.lang.String.format; +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration.GatewayConfigurationLoader.loadConfiguration; +import static org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration.NO_AUTHORIZATION; +import static org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration.TOKEN_AUTHORIZATION; + +/** + * Registers default services for the data plane provider proxy implementation. + */ +@Extension(value = ProxyProviderCoreExtension.NAME) +@Provides({GatewayConfigurationRegistry.class, AuthorizationHandlerRegistry.class}) +public class ProxyProviderCoreExtension implements ServiceExtension { + static final String NAME = "Data Plane Provider Proxy Core"; + + @Setting + private static final String PUBLIC_KEY = "tx.dpf.data.proxy.public.key"; + + @Inject(required = false) + private AuthorizationExtension authorizationExtension; + + @Inject + private Vault vault; + + @Inject + private Monitor monitor; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var configurationRegistry = new GatewayConfigurationRegistryImpl(); + context.registerService(GatewayConfigurationRegistry.class, configurationRegistry); + + if (authorizationExtension == null) { + context.getMonitor().info("Proxy JWT authorization is configured to only validate tokens and not provide path access control"); + authorizationExtension = (c, p) -> success(); + } + + var authorizationRegistry = creatAuthorizationRegistry(); + context.registerService(AuthorizationHandlerRegistry.class, authorizationRegistry); + + loadConfiguration(context).forEach(configuration -> { + monitor.info(format("Registering gateway configuration alias `%s` to %s", configuration.getAlias(), configuration.getProxiedPath())); + configurationRegistry.register(configuration); + }); + } + + @NotNull + private AuthorizationHandlerRegistryImpl creatAuthorizationRegistry() { + var authorizationRegistry = new AuthorizationHandlerRegistryImpl(); + + authorizationRegistry.register(NO_AUTHORIZATION, (t, p) -> success()); + + authorizationRegistry.register(TOKEN_AUTHORIZATION, createJwtAuthorizationHandler()); + + return authorizationRegistry; + } + + @NotNull + private AuthorizationHandler createJwtAuthorizationHandler() { + var publicCertKey = vault.resolveSecret(PUBLIC_KEY); + + if (publicCertKey == null) { + monitor.warning("Data proxy public key not set in the vault. Disabling JWT authorization for the proxy data."); + return (t, p) -> failure("Authentication disabled"); + } + + var publicKey = new RsaPublicKeyParser().parsePublicKey(publicCertKey); + var verifier = new RSASSAVerifier(publicKey); + + return new JwtAuthorizationHandler(verifier, authorizationExtension, monitor); + } + + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImpl.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImpl.java new file mode 100644 index 000000000..8c1878c73 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandler; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandlerRegistry; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation of the registry. + */ +public class AuthorizationHandlerRegistryImpl implements AuthorizationHandlerRegistry { + private final Map handlers = new HashMap<>(); + + @Override + public @Nullable AuthorizationHandler getHandler(String alias) { + return handlers.get(alias); + } + + @Override + public void register(String alias, AuthorizationHandler handler) { + handlers.put(alias, handler); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandler.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandler.java new file mode 100644 index 000000000..a4d1ca315 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationExtension; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandler; + +import java.text.ParseException; + +import static org.eclipse.edc.spi.result.Result.failure; + +/** + * Authenticates JWTs using a provided verifier and delegates to an {@link AuthorizationExtension} to provide access control checks for the requested path. + */ +public class JwtAuthorizationHandler implements AuthorizationHandler { + private final JWSVerifier verifier; + private final AuthorizationExtension authorizationExtension; + private final Monitor monitor; + + public JwtAuthorizationHandler(JWSVerifier verifier, AuthorizationExtension authorizationExtension, Monitor monitor) { + this.verifier = verifier; + this.authorizationExtension = authorizationExtension; + this.monitor = monitor; + } + + @Override + public Result authorize(String token, String path) { + try { + var jwt = SignedJWT.parse(token); + var result = jwt.verify(verifier); + + if (!result) { + return failure("Invalid token"); + } + + var claimToken = ClaimToken.Builder.newInstance() + .claims(jwt.getJWTClaimsSet().getClaims()) + .build(); + + return authorizationExtension.authorize(claimToken, path); + } catch (ParseException | JOSEException e) { + monitor.info("Invalid JWT received: " + e.getMessage()); + return failure("Invalid token"); + } + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParser.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParser.java new file mode 100644 index 000000000..23d0a2af9 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParser.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import org.eclipse.edc.spi.EdcException; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * A thread-safe parser than can read RSA public keys stored using PEM encoding. + */ +public class RsaPublicKeyParser { + private static final String HEADER = "-----BEGIN PUBLIC KEY-----"; + private static final String FOOTER = "-----END PUBLIC KEY-----"; + private final KeyFactory keyFactory; + + public RsaPublicKeyParser() { + try { + keyFactory = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new EdcException(e); + } + } + + /** + * Parses the PEM-encoded key. + */ + public RSAPublicKey parsePublicKey(String serialized) { + var keyPortion = serialized.replace(HEADER, "").replace(FOOTER, "").replaceAll("\\s", ""); + + var publicKeyDer = Base64.getDecoder().decode(keyPortion); + var spec = new X509EncodedKeySpec(publicKeyDer); + try { + return (RSAPublicKey) keyFactory.generatePublic(spec); + } catch (InvalidKeySpecException e) { + throw new EdcException(e); + } + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoader.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoader.java new file mode 100644 index 000000000..430b1c38a --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoader.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration; + +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration; + +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration.TOKEN_AUTHORIZATION; + +/** + * Loads gateway configuration from the {@link #TX_GATEWAY_PREFIX} prefix. + */ +public class GatewayConfigurationLoader { + static final String TX_GATEWAY_PREFIX = "tx.dpf.proxy.gateway"; + static final String AUTHORIZATION_TYPE = "authorization.type"; + static final String PROXIED_PATH = "proxied.path"; + + public static List loadConfiguration(ServiceExtensionContext context) { + var root = context.getConfig(TX_GATEWAY_PREFIX); + return root.partition().map(GatewayConfigurationLoader::createGatewayConfiguration).collect(toList()); + } + + private static GatewayConfiguration createGatewayConfiguration(Config config) { + return GatewayConfiguration.Builder.newInstance() + .alias(config.currentNode()) + .authorizationType(config.getString(AUTHORIZATION_TYPE, TOKEN_AUTHORIZATION)) + .proxiedPath(config.getString(PROXIED_PATH)) + .build(); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImpl.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImpl.java new file mode 100644 index 000000000..45a5b8d91 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration; + +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfigurationRegistry; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation. + */ +public class GatewayConfigurationRegistryImpl implements GatewayConfigurationRegistry { + private final Map configurations = new HashMap<>(); + + @Override + public @Nullable GatewayConfiguration getConfiguration(String alias) { + return configurations.get(alias); + } + + @Override + public void register(GatewayConfiguration configuration) { + configurations.put(configuration.getAlias(), configuration); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..5153c83eb --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,13 @@ + # + # Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + # + # 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: + # Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + +org.eclipse.tractusx.edc.dataplane.proxy.provider.core.ProxyProviderCoreExtension diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImplTest.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImplTest.java new file mode 100644 index 000000000..346481a55 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/AuthorizationHandlerRegistryImplTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationHandler; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class AuthorizationHandlerRegistryImplTest { + + @Test + void verify_registration() { + var registry = new AuthorizationHandlerRegistryImpl(); + registry.register("alias", mock(AuthorizationHandler.class)); + + assertThat(registry.getHandler("alias")).isNotNull(); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandlerTest.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandlerTest.java new file mode 100644 index 000000000..9b74fe54d --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/JwtAuthorizationHandlerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSVerifier; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization.AuthorizationExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class JwtAuthorizationHandlerTest { + private JwtAuthorizationHandler handler; + private AuthorizationExtension authExtension; + private JWSVerifier verifier; + + + @BeforeEach + void setUp() { + verifier = mock(JWSVerifier.class); + Monitor monitor = mock(Monitor.class); + authExtension = mock(AuthorizationExtension.class); + handler = new JwtAuthorizationHandler(verifier, authExtension, monitor); + } + + @Test + void verify_validCase() throws JOSEException { + when(verifier.verify(any(), any(), any())).thenReturn(true); + when(authExtension.authorize(isA(ClaimToken.class), eq("foo"))).thenReturn(success()); + + var result = handler.authorize(TestTokens.TEST_TOKEN, "foo"); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + void verify_parseInValidToken() throws JOSEException { + when(verifier.verify(any(), any(), any())).thenReturn(false); + + var result = handler.authorize(TestTokens.TEST_TOKEN, "foo"); + + assertThat(result.succeeded()).isFalse(); + } + + @Test + void verify_notAuthorized() throws JOSEException { + when(verifier.verify(any(), any(), any())).thenReturn(true); + when(authExtension.authorize(isA(ClaimToken.class), eq("foo"))).thenReturn(failure("Not authorized")); + + var result = handler.authorize(TestTokens.TEST_TOKEN, "foo"); + + assertThat(result.succeeded()).isFalse(); + + verify(authExtension).authorize(isA(ClaimToken.class), eq("foo")); + } + + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParserTest.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParserTest.java new file mode 100644 index 000000000..d345ce388 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/RsaPublicKeyParserTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies RSA public key parsing. + */ +class RsaPublicKeyParserTest { + + @Test + void verify_canParseKey() { + var key = new RsaPublicKeyParser().parsePublicKey(TestTokens.generatePublic()); + assertNotNull(key); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/TestTokens.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/TestTokens.java new file mode 100644 index 000000000..8db44bee8 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/auth/TestTokens.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.auth; + +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Tokens for testing. + */ +public class TestTokens { + private static final String DELIMITER = "-----"; + private static final String HEADER = DELIMITER + "BEGIN" + " PUBLIC " + "KEY" + DELIMITER + "\n"; + private static final String FOOTER = "\n" + DELIMITER + "END" + " PUBLIC " + "KEY" + DELIMITER + "\n"; + + public static final String TEST_TOKEN = "eyJhbGciOiJSUzI1NiIsInZlcnNpb24iOnRydWV9.eyJpc3MiOiJ0ZXN0LWNvbm5lY3RvciIsInN1YiI6ImNvbnN1bWVyLWNvbm5lY3RvciIsImF1ZCI6InRlc3QtY29ubmVjdG9yIiwiaWF0IjoxNjgxOTEzNjM2LCJleHAiOjMzNDU5NzQwNzg4LCJjaWQiOiIzMmE2M2E3ZC04MGQ2LTRmMmUtOTBlNi04MGJhZjVmYzJiM2MifQ.QAuotoRxpEqfuzkTcTq2w5Tcyy3Rc3UzUjjvNc_zwgNROGLe-wO9tFET1dJ_I5BttRxkngDS37dS4R6lN5YXaGHgcH2rf_FuVcJUSFqTp_usGAcx6m7pQQwqpNdcYgmq0NJp3xP87EFPHAy4kBxB5bqpmx4J-zrj9U_gerZ2WlRqpu0SdgP0S5v5D1Gm-vYkLqgvsugrAWH3Ti7OjC5UMdj0kDFwro2NpMY8SSNryiVvBEv8hn0KZdhhebIqPdhqbEQZ9d8WKzcgoqQ3DBd4ijzkd3Fz7ADD2gy_Hxn8Hi2LcItuB514TjCxYAncTNqZC_JSFEyuxwcGFVz3LdSXgw"; + + + public static String generatePublic() { + try { + var generator = KeyPairGenerator.getInstance("RSA"); + var pair = generator.generateKeyPair(); + var encoded = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()); + return HEADER + encoded + FOOTER; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoaderTest.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoaderTest.java new file mode 100644 index 000000000..b9eec317f --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationLoaderTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration; + +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration.GatewayConfigurationLoader.AUTHORIZATION_TYPE; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration.GatewayConfigurationLoader.PROXIED_PATH; +import static org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration.GatewayConfigurationLoader.TX_GATEWAY_PREFIX; +import static org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration.NO_AUTHORIZATION; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GatewayConfigurationLoaderTest { + + @Test + void verify_loadConfiguration() { + var context = mock(ServiceExtensionContext.class); + + var config = ConfigFactory.fromMap( + Map.of(format("alias.%s", AUTHORIZATION_TYPE), NO_AUTHORIZATION, + format("alias.%s", PROXIED_PATH), "https://test.com")); + when(context.getConfig(TX_GATEWAY_PREFIX)).thenReturn(config); + + var configurations = GatewayConfigurationLoader.loadConfiguration(context); + + assertThat(configurations).isNotEmpty(); + var configuration = configurations.get(0); + + assertThat(configuration.getAlias()).isEqualTo("alias"); + assertThat(configuration.getAuthorizationType()).isEqualTo(NO_AUTHORIZATION); + assertThat(configuration.getProxiedPath()).isEqualTo("https://test.com"); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImplTest.java b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImplTest.java new file mode 100644 index 000000000..9bd58c365 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-core/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/provider/core/gateway/configuration/GatewayConfigurationRegistryImplTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.provider.core.gateway.configuration; + +import org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration.GatewayConfiguration; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GatewayConfigurationRegistryImplTest { + + @Test + void verify_Configuration() { + var registry = new GatewayConfigurationRegistryImpl(); + registry.register(GatewayConfiguration.Builder.newInstance().proxiedPath("https://test.com").alias("alias").build()); + + assertThat(registry.getConfiguration("alias")).isNotNull(); + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/build.gradle.kts b/edc-dataplane/edc-dataplane-proxy-provider-spi/build.gradle.kts new file mode 100644 index 000000000..9ca2f9437 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(libs.edc.spi.core) +} + diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationExtension.java b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationExtension.java new file mode 100644 index 000000000..37ffc5490 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationExtension.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization; + +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.result.Result; + +/** + * Performs an authorization check for the given path against a set of claims. + */ +public interface AuthorizationExtension { + + /** + * Performs an authorization check for the given path against the presented claims. The path is the request alias path, not + * the proxied path. + * + * @param token the validated claim token + * @param path the request alias path, not the dereferenced proxied path + */ + Result authorize(ClaimToken token, String path); + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandler.java b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandler.java new file mode 100644 index 000000000..5442ebc98 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization; + +import org.eclipse.edc.spi.result.Result; + +/** + * Performs an authorization using the request token for a given path. Implementation support different token formats such as JWT. + */ +@FunctionalInterface +public interface AuthorizationHandler { + + /** + * Performs the authorization check. + * + * @param token the unvalidated token + * @param path the request alias path, not the dereferenced proxied path + */ + Result authorize(String token, String path); + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandlerRegistry.java b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandlerRegistry.java new file mode 100644 index 000000000..b40217cb4 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/authorization/AuthorizationHandlerRegistry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.authorization; + +import org.jetbrains.annotations.Nullable; + +/** + * Manages {@link AuthorizationHandler}s. + */ +public interface AuthorizationHandlerRegistry { + + /** + * Returns a handler for the alias or null if not found. + */ + @Nullable + AuthorizationHandler getHandler(String alias); + + /** + * Registers a handler for the given alias. + */ + void register(String alias, AuthorizationHandler handler); + +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfiguration.java b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfiguration.java new file mode 100644 index 000000000..baafbf4b6 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration; + +import static java.util.Objects.requireNonNull; + +/** + * A configuration that exposes a proxied endpoint via an alias. Each configuration is associated with an extensible {@code authorizationType} such as + * {@link #TOKEN_AUTHORIZATION} (the default) and {@link #NO_AUTHORIZATION}. The {@code proxiedPath} will be prepended to a request sub-path to create an absolute endpoint + * URL where data is fetched from. + */ +public class GatewayConfiguration { + public static final String TOKEN_AUTHORIZATION = "token"; + public static final String NO_AUTHORIZATION = "none"; + + private String alias; + private String proxiedPath; + private String authorizationType = TOKEN_AUTHORIZATION; + + public String getAlias() { + return alias; + } + + public String getProxiedPath() { + return proxiedPath; + } + + public String getAuthorizationType() { + return authorizationType; + } + + private GatewayConfiguration() { + } + + public static class Builder { + + private final GatewayConfiguration configuration; + + public static Builder newInstance() { + return new Builder(); + } + + public Builder alias(String alias) { + this.configuration.alias = alias; + return this; + } + + public Builder proxiedPath(String proxiedPath) { + this.configuration.proxiedPath = proxiedPath; + return this; + } + + public Builder authorizationType(String authorizationType) { + this.configuration.authorizationType = authorizationType; + return this; + } + + public GatewayConfiguration build() { + requireNonNull(configuration.alias, "alias"); + requireNonNull(configuration.proxiedPath, "proxiedPath"); + requireNonNull(configuration.authorizationType, "authorizationType"); + return configuration; + } + + private Builder() { + configuration = new GatewayConfiguration(); + } + } +} diff --git a/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfigurationRegistry.java b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfigurationRegistry.java new file mode 100644 index 000000000..d96ddf730 --- /dev/null +++ b/edc-dataplane/edc-dataplane-proxy-provider-spi/src/main/java/org/eclipse/tractusx/edc/dataplane/proxy/spi/provider/gateway/configuration/GatewayConfigurationRegistry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.spi.provider.gateway.configuration; + +import org.jetbrains.annotations.Nullable; + +/** + * Manages {@link GatewayConfiguration}s. + */ +public interface GatewayConfigurationRegistry { + + /** + * Returns the configuration for the given alias or null if not found. + */ + @Nullable + GatewayConfiguration getConfiguration(String alias); + + /** + * Registers a configuration for the given alias. + */ + void register(GatewayConfiguration configuration); + +} diff --git a/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts b/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts new file mode 100644 index 000000000..50bd6598d --- /dev/null +++ b/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(libs.okhttp.mockwebserver) + + // test runtime config + testImplementation(libs.edc.config.filesystem) + testImplementation(libs.edc.vault.filesystem) + testImplementation(libs.edc.dpf.http) + testImplementation(project(":spi:edr-cache-spi")) + testImplementation(project(":core:edr-cache-core")) + testImplementation(project(":edc-dataplane:edc-dataplane-proxy-consumer-api")) + testImplementation(project(":edc-dataplane:edc-dataplane-proxy-provider-api")) + testImplementation(project(":edc-dataplane:edc-dataplane-proxy-provider-core")) + +} + + + diff --git a/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/DpfProxyEndToEndTest.java b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/DpfProxyEndToEndTest.java new file mode 100644 index 000000000..c7649ea67 --- /dev/null +++ b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/DpfProxyEndToEndTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.e2e; + +import io.restassured.specification.RequestSpecification; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; +import org.eclipse.tractusx.edc.edr.spi.EndpointDataReferenceCache; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static java.lang.String.format; +import static java.lang.String.valueOf; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import static org.eclipse.tractusx.edc.dataplane.proxy.e2e.EdrCacheSetup.createEntries; +import static org.eclipse.tractusx.edc.dataplane.proxy.e2e.KeyStoreSetup.createKeyStore; +import static org.eclipse.tractusx.edc.dataplane.proxy.e2e.VaultSetup.createVaultStore; +import static org.hamcrest.Matchers.is; + + +/** + * Performs end-to-end testing using a consumer data plane, a producer data plane, and a proxied HTTP endpoint. + *

+ * The consumer runtime is configured with three EDRs: + *

    + *
  • One EDR is for the {@link #SINGLE_TRANSFER_ID} transfer process that is associated with a single contract agreement for the {@link #SINGLE_ASSET_ID} + * asset
  • + *
  • Two EDRs for transfer processes that are associated with contract agreements for the same asset, {@link #MULTI_ASSET_ID}
  • + *
+ *

+ * The end-to-end tests verify asset content is correctly proxied from the HTTP endpoint, error messages from the HTTP endpoint are correctly propagated, + * and invalid requests are properly handled. + *

+ * This test can be executed using the Gradle or JUnit test runners. + */ +@EndToEndTest +public class DpfProxyEndToEndTest { + private static final String LAUNCHER_MODULE = ":edc-tests:edc-dataplane-proxy-e2e"; + + private static final int CONSUMER_HTTP_PORT = getFreePort(); + private static final int CONSUMER_PROXY_PORT = getFreePort(); + private static final int PRODUCER_HTTP_PORT = getFreePort(); + private static final int MOCK_ENDPOINT_PORT = getFreePort(); + + private static final String PROXY_SUBPATH = "proxy/aas/request"; + + private static final String SINGLE_TRANSFER_ID = "5355d524-2616-43df-9096-558afffff659"; + private static final String SINGLE_ASSET_ID = "79f13b89-59a6-4278-8c8e-8540849dbab8"; + private static final String MULTI_ASSET_ID = "9260f395-3d94-4b8b-bdaa-941ead596ce5"; + + private static final String REQUEST_TEMPLATE_TP = "{\"transferProcessId\": \"%s\", \"endpointUrl\" : \"http://localhost:%s/api/gateway/aas/test\"}"; + private static final String REQUEST_TEMPLATE_ASSET = "{\"assetId\": \"%s\", \"endpointUrl\" : \"http://localhost:%s/api/gateway/aas/test\"}"; + + private static final String MOCK_ENDPOINT_200_BODY = "{\"message\":\"test\"}"; + + public static final String KEYSTORE_PASS = "test123"; + + private MockWebServer mockEndpoint; + + @RegisterExtension + static EdcRuntimeExtension CONSUMER = new EdcRuntimeExtension( + LAUNCHER_MODULE, + "consumer", + baseConfig(Map.of( + "web.http.port", valueOf(CONSUMER_HTTP_PORT), + "tx.dpf.consumer.proxy.port", valueOf(CONSUMER_PROXY_PORT) + ))); + + @RegisterExtension + static EdcRuntimeExtension PROVIDER = new EdcRuntimeExtension( + LAUNCHER_MODULE, + "provider", + baseConfig(Map.of( + "web.http.port", valueOf(PRODUCER_HTTP_PORT), + "tx.dpf.proxy.gateway.aas.proxied.path", "http://localhost:" + MOCK_ENDPOINT_PORT + ))); + + @BeforeEach + void setUp() { + mockEndpoint = new MockWebServer(); + } + + @AfterEach + void tearDown() throws IOException { + if (mockEndpoint != null) { + mockEndpoint.close(); + } + } + + @Test + void verify_end2EndFlows() throws IOException { + + seedEdrCache(); + + // set up the HTTP endpoint + mockEndpoint.enqueue(new MockResponse().setBody(MOCK_ENDPOINT_200_BODY)); + mockEndpoint.enqueue(new MockResponse().setBody(MOCK_ENDPOINT_200_BODY)); + mockEndpoint.enqueue(new MockResponse().setResponseCode(404)); + mockEndpoint.enqueue(new MockResponse().setResponseCode(401)); + mockEndpoint.start(MOCK_ENDPOINT_PORT); + + var tpSpec = createSpecification(format(REQUEST_TEMPLATE_TP, SINGLE_TRANSFER_ID, PRODUCER_HTTP_PORT)); + + // verify content successfully proxied using a transfer process id + tpSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(200) + .assertThat().body(is(MOCK_ENDPOINT_200_BODY)); + + // verify content successfully proxied using an asset id for the case where only one active transfer process exists for the asset + var assetSpec = createSpecification(format(REQUEST_TEMPLATE_ASSET, SINGLE_ASSET_ID, PRODUCER_HTTP_PORT)); + assetSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(200) + .assertThat().body(is(MOCK_ENDPOINT_200_BODY)); + + // verify content not found (404) response at the endpoint is propagated + tpSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(404); + + // verify unauthorized response (403) at the endpoint is propagated + tpSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(401); + + // verify EDR not found results in a bad request response (400) + var invalidSpec = createSpecification(format(REQUEST_TEMPLATE_TP, "123", PRODUCER_HTTP_PORT)); + invalidSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(400); + + // verify more than one contract for the same asset results in a precondition required response (428) + var multiAssetSpec = createSpecification(format(REQUEST_TEMPLATE_ASSET, MULTI_ASSET_ID, PRODUCER_HTTP_PORT)); + multiAssetSpec.with() + .post(PROXY_SUBPATH) + .then() + .assertThat().statusCode(428); + } + + private RequestSpecification createSpecification(String body) { + return given() + .baseUri("http://localhost:" + CONSUMER_PROXY_PORT) + .contentType("application/json") + .body(body); + } + + private static Map baseConfig(Map values) { + var map = new HashMap<>(values); + map.put("edc.vault", createVaultStore()); + map.put("edc.keystore", createKeyStore(KEYSTORE_PASS)); + map.put("edc.keystore.password", KEYSTORE_PASS); + return map; + } + + /** + * Loads the EDR cache. + */ + private void seedEdrCache() { + var edrCache = CONSUMER.getContext().getService(EndpointDataReferenceCache.class); + createEntries().forEach(e->edrCache.save(e.getEdrEntry(), e.getEdr())); + } + + + + +} diff --git a/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/EdrCacheSetup.java b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/EdrCacheSetup.java new file mode 100644 index 000000000..bcaeaf68a --- /dev/null +++ b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/EdrCacheSetup.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.e2e; + +import org.eclipse.edc.spi.types.domain.edr.EndpointDataReference; +import org.eclipse.tractusx.edc.edr.core.defaults.PersistentCacheEntry; +import org.eclipse.tractusx.edc.edr.spi.EndpointDataReferenceEntry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates test EDR cache entries. + */ +public class EdrCacheSetup { + + public static final String AUTHENTICATION = "authentication"; + public static final String ENDPOINT = "http://test.com"; + + public static List createEntries() { + var list = new ArrayList(); + + var edrEntry = EndpointDataReferenceEntry.Builder.newInstance() + .assetId("79f13b89-59a6-4278-8c8e-8540849dbab8") + .agreementId("a62d02a3-eea5-4852-86d4-5482db4dffe8") + .transferProcessId("5355d524-2616-43df-9096-558afffff659") + .build(); + var edr = EndpointDataReference.Builder.newInstance() + .id("c470e649-5454-4e4d-b065-782752e5d759") + .endpoint(ENDPOINT) + .authKey(AUTHENTICATION) + .authCode(generateAuthCode()) + .build(); + list.add(new PersistentCacheEntry(edrEntry, edr)); + + var edrEntry2 = EndpointDataReferenceEntry.Builder.newInstance() + .assetId("9260f395-3d94-4b8b-bdaa-941ead596ce5") + .agreementId("d6f73f25-b0aa-4b62-843f-7cfaba532b5b8") + .transferProcessId("b2859c0a-1a4f-4d10-a3fd-9652d7b3469a") + .build(); + var edr2 = EndpointDataReference.Builder.newInstance() + .id("514a4142-3d2a-4936-97c3-7892961c6a58") + .endpoint(ENDPOINT) + .authKey(AUTHENTICATION) + .authCode(generateAuthCode()) + .build(); + list.add(new PersistentCacheEntry(edrEntry2, edr2)); + + var edrEntry3 = EndpointDataReferenceEntry.Builder.newInstance() + .assetId("9260f395-3d94-4b8b-bdaa-941ead596ce5") + .agreementId("7a23333b-03b5-4547-822b-595a54ad6d38") + .transferProcessId("7a23333b-03b5-4547-822b-595a54ad6d38") + .build(); + var edr3 = EndpointDataReference.Builder.newInstance() + .id("3563c5a1-685d-40e5-a380-0b5761523d2d") + .endpoint(ENDPOINT) + .authKey(AUTHENTICATION) + .authCode(generateAuthCode()) + .build(); + list.add(new PersistentCacheEntry(edrEntry3, edr3)); + + + return list; + } + + private static String generateAuthCode() { + //noinspection StringBufferReplaceableByString + return new StringBuilder() + .append("eyJhbGciOiJSUzI1NiIsInZlcn") + .append("Npb24iOnRydWV9.") + .append("eyJpc3MiOiJ0ZXN0LWNvb") + .append("m5lY3RvciIsInN1YiI6ImNvbnN1bW") + .append("VyLWNvbm5lY3RvciIsImF1ZCI6InRlc3Q") + .append("tY29ubmVjdG9yIiwi") + .append("aWF0IjoxNjgxOTEzN") + .append("jM2LCJleHAiOjMzNDU5NzQwNzg4LCJjaWQiOiIzMmE2M") + .append("2E3ZC04MGQ2LTRmMmUtOTBlN") + .append("i04MGJhZjVmYzJiM2MifQ.QAuotoRxpEqfuzkTcTq2w5Tcyy") + .append("3Rc3UzUjjvNc_zwgNROGLe-wO") + .append("9tFET1dJ_I5BttRxkngDS37dS4R6lN5YXaGHgcH2rf_FuVcJUS") + .append("FqTp_usGAcx6m7pQQwqpNdcYgmq0NJp3xP87EFP") + .append("HAy4kBxB5bqpmx4J-zrj9U_gerZ2WlRqpu0SdgP0S5v5D1Gm-v") + .append("YkLqgvsugrAWH3Ti7OjC5UMdj0kDFwro2NpMY8SSNryiVvBEv8hn0KZdhhebIqPd") + .append("hqbEQZ9d8WKzcgoqQ3DBd4ijzkd3Fz7ADD2gy_Hxn8Hi2LcItuB514TjCxYA") + .append("ncTNqZC_JSFEyuxwcGFVz3LdSXgw") + .toString(); + } +} + diff --git a/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/KeyStoreSetup.java b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/KeyStoreSetup.java new file mode 100644 index 000000000..b2d6da193 --- /dev/null +++ b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/KeyStoreSetup.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.e2e; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; + +/** + * Sets up a test keystore. + */ +public class KeyStoreSetup { + + public static String createKeyStore(String password) { + try { + var ks = KeyStore.getInstance(KeyStore.getDefaultType()); + + ks.load(null, password.toCharArray()); + + var file = File.createTempFile("test", "-keystore.jks"); + try (var fos = new FileOutputStream(file)) { + ks.store(fos, password.toCharArray()); + } + file.deleteOnExit(); + return file.getAbsolutePath(); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private KeyStoreSetup() { + } +} diff --git a/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/VaultSetup.java b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/VaultSetup.java new file mode 100644 index 000000000..8bbae5f09 --- /dev/null +++ b/edc-tests/edc-dataplane-proxy-e2e/src/test/java/org/eclipse/tractusx/edc/dataplane/proxy/e2e/VaultSetup.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.dataplane.proxy.e2e; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Generates a test vault implementation. + */ +public class VaultSetup { + private static final String DELIMITER = "-----"; + private static final String HEADER = DELIMITER + "BEGIN" + " PUBLIC " + "KEY" + DELIMITER; + private static final String FOOTER = DELIMITER + "END" + " PUBLIC " + "KEY" + DELIMITER; + + private static final String VAULT_CONTENTS = "tx.dpf.data.proxy.public.key=" + HEADER + "\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyMkG7DSIhMjFOtqQJsr+\\nKtzfKKGGQ/7mBdjwDCEj0ijKLG/LiEYWsbPA8L/oMAIdR4xpLGaajtz6wj7NbMiA\\nrtHF1HA3mNoeKGix7SfobfQ9J7gJJmSE5DA4BxatL4sPMfoV2SJanJQQjOEAA6/i\\nI+o8SeeBc/2YE55O3yeFjdHK5JIwDi9vIkGnDRBd9poyrHYV+7dcyBB45r6BwvoW\\nG41mezzlKbOl0ZtPW1T9fqp+lOiZWIHMY5ml1daGSbTWwfJxc7XfHHa8KCNQcsPR\\nhWYx6PnxvgqQwYPjvqZF7OYAMUOQX8pg6jfYiU4HgUI1jwwGw3UpJq4b3kzD3u4T\\nDQIDAQAB\\n" + FOOTER + "\n"; + + public static String createVaultStore() { + try { + var file = File.createTempFile("test", "-vault.properties"); + try (var writer = new FileWriter(file)) { + writer.write(VAULT_CONTENTS); + } + file.deleteOnExit(); + return file.getAbsolutePath(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private VaultSetup() { + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bfca01267..e3bf7740e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,9 @@ edc-spi-transactionspi = { module = "org.eclipse.edc:transaction-spi", version.r edc-spi-aggregateservices = { module = "org.eclipse.edc:aggregate-service-spi", version.ref = "edc" } edc-spi-controlplane = { module = "org.eclipse.edc:control-plane-spi", version.ref = "edc" } edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } +edc-spi-http = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } edc-spi-jwt = { module = "org.eclipse.edc:jwt-spi", version.ref = "edc" } +edc-jwt-core = { module = "org.eclipse.edc:jwt-core", version.ref = "edc" } edc-spi-oauth2 = { module = "org.eclipse.edc:oauth2-spi", version.ref = "edc" } edc-spi-ids = { module = "org.eclipse.edc:ids-spi", version.ref = "edc" } edc-util = { module = "org.eclipse.edc:util", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 84f69e546..f1fbd33f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,11 @@ include(":edc-dataplane") include(":edc-dataplane:edc-dataplane-azure-vault") include(":edc-dataplane:edc-dataplane-base") include(":edc-dataplane:edc-dataplane-hashicorp-vault") +include(":edc-dataplane:edc-dataplane-proxy-consumer-api") +include(":edc-dataplane:edc-dataplane-proxy-provider-spi") +include(":edc-dataplane:edc-dataplane-proxy-provider-core") +include(":edc-dataplane:edc-dataplane-proxy-provider-api") +include(":edc-tests:edc-dataplane-proxy-e2e") // Version Catalog include(":version-catalog")