From aab1736b072c9657fe0be4f964efe1a8d96ea5ed Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 16 Mar 2024 17:48:21 +0100 Subject: [PATCH] Fixes: no data exchange possible (#95) * Give controller correct config * Cleanup, fix smaller bugs * Temporary fix of multiple AuthRequestFilters * Add unified auth request filter Inside public-api-management extension * Remove extension specific authreq-filter Also add unified one in dependencies --- client/build.gradle.kts | 5 +- .../iosb/client/ClientEndpoint.java | 45 +++--- .../iosb/client/ClientExtension.java | 66 ++++----- ....java => DataTransferEndpointManager.java} | 51 ++----- .../dataTransfer/DataTransferController.java | 54 ++++--- .../dataTransfer/TransferInitiator.java | 54 ++++--- .../iosb/client/negotiation/Negotiator.java | 2 - .../iosb/client/ClientEndpointTest.java | 9 +- .../iosb/client/ClientExtensionTest.java | 6 +- .../dataTransfer/TransferInitiatorTest.java | 2 +- edc-extension4aas/build.gradle.kts | 3 + .../de/fraunhofer/iosb/app/AasExtension.java | 49 ++++--- .../CustomAuthenticationRequestFilter.java | 63 --------- ...CustomAuthenticationRequestFilterTest.java | 114 --------------- ...ataspaceconnector-configuration.properties | 4 +- public-api-management/build.gradle.kts | 40 ++++++ .../api/PublicApiManagementExtension.java | 57 ++++++++ .../iosb/api/PublicApiManagementService.java | 74 ++++++++++ .../CustomAuthenticationRequestFilter.java | 96 +++++++++++++ .../fraunhofer/iosb/api/model/Endpoint.java | 67 +++++++++ .../fraunhofer/iosb/api/model/HttpMethod.java | 50 +++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + ...CustomAuthenticationRequestFilterTest.java | 132 ++++++++++++++++++ .../iosb/api/model/EndpointTest.java | 97 +++++++++++++ settings.gradle.kts | 3 +- 25 files changed, 791 insertions(+), 353 deletions(-) rename client/src/main/java/de/fraunhofer/iosb/client/authentication/{CustomAuthenticationRequestFilter.java => DataTransferEndpointManager.java} (51%) delete mode 100644 edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilter.java delete mode 100644 edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilterTest.java create mode 100644 public-api-management/build.gradle.kts create mode 100644 public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementExtension.java create mode 100644 public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementService.java create mode 100644 public-api-management/src/main/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilter.java create mode 100644 public-api-management/src/main/java/de/fraunhofer/iosb/api/model/Endpoint.java create mode 100644 public-api-management/src/main/java/de/fraunhofer/iosb/api/model/HttpMethod.java create mode 100644 public-api-management/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 public-api-management/src/test/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilterTest.java create mode 100644 public-api-management/src/test/java/de/fraunhofer/iosb/api/model/EndpointTest.java diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 7ae43a80..9195bf0e 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -16,13 +16,16 @@ java { } dependencies { + + // Centralized auth request filter + implementation(project(":public-api-management")) + // See this project's README.MD for explanations implementation("$group:contract-core:$edcVersion") implementation("$group:dsp-catalog-http-dispatcher:$edcVersion") implementation("$group:management-api:$edcVersion") implementation("$group:runtime-metamodel:$edcVersion") implementation("$group:data-plane-http-spi:$edcVersion") // HttpDataAddress - implementation("jakarta.ws.rs:jakarta.ws.rs-api:${rsApi}") testImplementation("$group:junit:$edcVersion") diff --git a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java index b3b7b1d4..7ea7eb45 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java @@ -47,8 +47,8 @@ /** * Automated contract negotiation */ -@Consumes({ MediaType.APPLICATION_JSON, MediaType.WILDCARD }) -@Produces({ MediaType.APPLICATION_JSON }) +@Consumes({MediaType.APPLICATION_JSON, MediaType.WILDCARD}) +@Produces({MediaType.APPLICATION_JSON}) @Path(ClientEndpoint.AUTOMATED_PATH) public class ClientEndpoint { /* @@ -70,15 +70,15 @@ public class ClientEndpoint { /** * Initialize a client endpoint. * - * @param policyService Finds out policy for a given asset id and provider - * EDC url. - * @param negotiator Send contract offer, negotiation status watch. - * @param transferInitiator Initiate transfer requests. + * @param monitor Logging functionality + * @param negotiationController Send contract offer, negotiation status watch. + * @param policyController Provides API for accepted policy management and provider dataset retrieval. + * @param transferController Initiate transfer requests. */ public ClientEndpoint(Monitor monitor, - NegotiationController negotiationController, - PolicyController policyController, - DataTransferController transferController) { + NegotiationController negotiationController, + PolicyController policyController, + DataTransferController transferController) { this.monitor = monitor; this.policyController = policyController; @@ -91,13 +91,10 @@ public ClientEndpoint(Monitor monitor, * of the services' policyDefinitionStore instance containing user added * policyDefinitions. If more than one policyDefinitions are provided by the * provider connector, an AmbiguousOrNullException will be thrown. - * + * * @param providerUrl Provider of the asset. * @param assetId Asset ID of the asset whose contract should be fetched. * @return One policyDefinition offered by the provider for the given assetId. - * @throws InterruptedException Thread for agreementId was waiting, sleeping, or - * otherwise occupied, and was - * interrupted. */ @GET @Path(DATASET_PATH) @@ -123,18 +120,18 @@ public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryPar * Negotiate a contract agreement using the given contract offer if no agreement * exists for this constellation. * - * @param providerUrl Provider EDCs URL (DSP endpoint) - * @param providerId Provider EDCs ID - * @param assetId ID of the asset to be retrieved + * @param providerUrl Provider EDCs URL (DSP endpoint) + * @param providerId Provider EDCs ID + * @param assetId ID of the asset to be retrieved * @param dataDestinationUrl URL of destination data sink. * @return Asset data */ @POST @Path(NEGOTIATE_PATH) public Response negotiateContract(@QueryParam("providerUrl") URL providerUrl, - @QueryParam("providerId") String providerId, - @QueryParam("assetId") String assetId, - @QueryParam("dataDestinationUrl") URL dataDestinationUrl) { + @QueryParam("providerId") String providerId, + @QueryParam("assetId") String assetId, + @QueryParam("dataDestinationUrl") URL dataDestinationUrl) { monitor.debug(format("[Client] Received a %s POST request", NEGOTIATE_PATH)); Objects.requireNonNull(providerUrl, "Provider URL must not be null"); Objects.requireNonNull(providerId, "Provider ID must not be null"); @@ -205,17 +202,17 @@ public Response negotiateContract(ContractRequest contractRequest) { /** * Submits a data transfer request to the providerUrl. * - * @param providerUrl The data provider's url - * @param agreementId The basis of the data transfer. - * @param assetId The asset of which the data should be transferred + * @param providerUrl The data provider's url + * @param agreementId The basis of the data transfer. + * @param assetId The asset of which the data should be transferred * @param dataDestinationUrl URL of destination data sink. * @return On success, the data of the desired asset. Else, returns an error message. */ @GET @Path(TRANSFER_PATH) public Response getData(@QueryParam("providerUrl") URL providerUrl, - @QueryParam("agreementId") String agreementId, @QueryParam("assetId") String assetId, - @QueryParam("dataDestinationUrl") URL dataDestinationUrl) { + @QueryParam("agreementId") String agreementId, @QueryParam("assetId") String assetId, + @QueryParam("dataDestinationUrl") URL dataDestinationUrl) { monitor.debug(format("[Client] Received a %s GET request", TRANSFER_PATH)); Objects.requireNonNull(providerUrl, "providerUrl must not be null"); Objects.requireNonNull(agreementId, "agreementId must not be null"); diff --git a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java index 3dd364fa..80bc3f94 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java @@ -15,7 +15,10 @@ */ package de.fraunhofer.iosb.client; -import org.eclipse.edc.api.auth.spi.AuthenticationService; +import de.fraunhofer.iosb.api.PublicApiManagementService; +import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +import de.fraunhofer.iosb.client.negotiation.NegotiationController; +import de.fraunhofer.iosb.client.policy.PolicyController; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; @@ -27,45 +30,42 @@ import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.web.spi.WebService; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; -import de.fraunhofer.iosb.client.negotiation.NegotiationController; -import de.fraunhofer.iosb.client.policy.PolicyController; - public class ClientExtension implements ServiceExtension { - @Inject - private AuthenticationService authenticationService; - @Inject - private CatalogService catalogService; - @Inject - private ConsumerContractNegotiationManager consumerNegotiationManager; - @Inject - private ContractNegotiationObservable contractNegotiationObservable; - @Inject - private ContractNegotiationStore contractNegotiationStore; - @Inject - private TransferProcessManager transferProcessManager; - @Inject - private TypeTransformerRegistry transformer; - @Inject - private WebService webService; + // Non-public unified authentication request filter management service + @Inject + private PublicApiManagementService publicApiManagementService; - @Override - public void initialize(ServiceExtensionContext context) { - var monitor = context.getMonitor(); - var config = context.getConfig("edc.client"); + @Inject + private CatalogService catalogService; + @Inject + private ConsumerContractNegotiationManager consumerNegotiationManager; + @Inject + private ContractNegotiationObservable contractNegotiationObservable; + @Inject + private ContractNegotiationStore contractNegotiationStore; + @Inject + private TransferProcessManager transferProcessManager; + @Inject + private TypeTransformerRegistry transformer; + @Inject + private WebService webService; - var policyController = new PolicyController(monitor, catalogService, transformer, config); + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + var config = context.getConfig("edc.client"); - var negotiationController = new NegotiationController(consumerNegotiationManager, - contractNegotiationObservable, contractNegotiationStore, config); + var policyController = new PolicyController(monitor, catalogService, transformer, config); - var dataTransferController = new DataTransferController(monitor, config, webService, - authenticationService, transferProcessManager); + var negotiationController = new NegotiationController(consumerNegotiationManager, + contractNegotiationObservable, contractNegotiationStore, config); - webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, - dataTransferController)); + var dataTransferController = new DataTransferController(monitor, context.getConfig(), webService, + publicApiManagementService, transferProcessManager, context.getConnectorId()); - } + webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, + dataTransferController)); + } } diff --git a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java b/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java similarity index 51% rename from client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java rename to client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java index 63b8a74e..4872ebc1 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java @@ -15,6 +15,9 @@ */ package de.fraunhofer.iosb.client.authentication; +import de.fraunhofer.iosb.api.PublicApiManagementService; +import de.fraunhofer.iosb.api.model.Endpoint; +import de.fraunhofer.iosb.api.model.HttpMethod; import de.fraunhofer.iosb.client.ClientEndpoint; import de.fraunhofer.iosb.client.dataTransfer.DataTransferEndpoint; import jakarta.ws.rs.container.ContainerRequestContext; @@ -22,6 +25,7 @@ import org.eclipse.edc.api.auth.spi.AuthenticationService; import org.eclipse.edc.spi.monitor.Monitor; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -32,49 +36,24 @@ * Custom AuthenticationRequestFilter filtering requests that go directly to an * AAS service (managed by this extension) or the extension's configuration. */ -public class CustomAuthenticationRequestFilter extends AuthenticationRequestFilter { +public class DataTransferEndpointManager { - private final Monitor monitor; - private final Map tempKeys; + private final PublicApiManagementService publicApiManagementService; - public CustomAuthenticationRequestFilter(Monitor monitor, AuthenticationService authenticationService) { - super(authenticationService); - this.monitor = monitor; - tempKeys = new ConcurrentHashMap<>(); + public DataTransferEndpointManager(PublicApiManagementService publicApiManagementService) { + this.publicApiManagementService = publicApiManagementService; } /** * Add key,value pair for a request. This key will only be available for one * request. - * - * @param key The key name - * @param value The actual key - */ - public void addTemporaryApiKey(String key, String value) { - tempKeys.put(key, value); - } - /** - * On automated data transfer: If the request is valid, the key,value pair used - * for this request will no longer be valid. + * @param agreementId Agreement to build the endpoint path suffix + * @param key The key name + * @param value The value */ - @Override - public void filter(ContainerRequestContext requestContext) { - Objects.requireNonNull(requestContext); - var requestPath = requestContext.getUriInfo().getPath(); - - for (String key : tempKeys.keySet()) { - if (requestContext.getHeaders().containsKey(key) - && requestContext.getHeaderString(key).equals(tempKeys.get(key)) - && requestPath.startsWith( - format("%s/%s", ClientEndpoint.AUTOMATED_PATH, DataTransferEndpoint.RECEIVE_DATA_PATH))) { - monitor.debug( - format("[Client] Data Transfer request with custom api key %s", key)); - tempKeys.remove(key); - return; - } - } - - super.filter(requestContext); + public void addTemporaryEndpoint(String agreementId, String key, String value) { + var endpointSuffix = ClientEndpoint.AUTOMATED_PATH + "/receiveData/" + agreementId; + publicApiManagementService.addTemporaryEndpoint(new Endpoint(endpointSuffix, HttpMethod.POST, Map.of(key, List.of(value)))); } -} +} \ No newline at end of file diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferController.java b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferController.java index 9592647d..1d5721cb 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferController.java @@ -25,7 +25,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.eclipse.edc.api.auth.spi.AuthenticationService; +import de.fraunhofer.iosb.api.PublicApiManagementService; +import de.fraunhofer.iosb.client.authentication.DataTransferEndpointManager; import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; import org.eclipse.edc.spi.EdcException; @@ -33,8 +34,6 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.web.spi.WebService; -import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; - public class DataTransferController { static final String DATA_TRANSFER_API_KEY = "data-transfer-api-key"; @@ -46,27 +45,26 @@ public class DataTransferController { private final DataTransferObservable dataTransferObservable; private final TransferInitiator transferInitiator; - private final CustomAuthenticationRequestFilter dataEndpointAuthenticationRequestFilter; + private final DataTransferEndpointManager dataTransferEndpointManager; /** * Class constructor * - * @param monitor Logging. - * @param config Read config value transfer timeout and - * own URI - * @param webService Register data transfer endpoint. - * @param authenticationService Creating and passing through custom api - * keys for each data transfer. - * @param transferProcessManager Initiating a transfer process as a - * consumer. + * @param monitor Logging. + * @param config Read config value transfer timeout and + * own URI + * @param webService Register data transfer endpoint. + * @param publicApiManagementService Creating and passing through custom api + * keys for each data transfer. + * @param transferProcessManager Initiating a transfer process as a + * consumer. + * @param connectorId Connector ID for the provider to learn */ public DataTransferController(Monitor monitor, Config config, WebService webService, - AuthenticationService authenticationService, TransferProcessManager transferProcessManager) { - this.config = config; - this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); - this.dataEndpointAuthenticationRequestFilter = new CustomAuthenticationRequestFilter(monitor, - authenticationService); - + PublicApiManagementService publicApiManagementService, TransferProcessManager transferProcessManager, String connectorId) { + this.config = config.getConfig("edc.client"); + this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager, connectorId); + this.dataTransferEndpointManager = new DataTransferEndpointManager(publicApiManagementService); this.dataTransferObservable = new DataTransferObservable(monitor); var dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); webService.registerResource(dataTransferEndpoint); @@ -76,26 +74,24 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ * Initiates the transfer process defined by the arguments. The data of the * transfer will be sent to {@link DataTransferEndpoint#RECEIVE_DATA_PATH}. * - * @param providerUrl The provider from whom the data is to be fetched. - * @param agreementId Non-null ContractAgreement of the negotiation process. - * @param assetId The asset to be fetched. - * @param dataSinkAddress HTTPDataAddress the result of the transfer should be - * sent to. (If null, send to extension and print in log) - * - * @return A completable future whose result will be the data or an error - * message. + * @param providerUrl The provider from whom the data is to be fetched. + * @param agreementId Non-null ContractAgreement of the negotiation process. + * @param assetId The asset to be fetched. + * @param dataDestinationUrl HTTPDataAddress the result of the transfer should be + * sent to. (If null, send to extension and print in log) + * @return A completable future whose result will be the data or an error message. * @throws InterruptedException If the data transfer was interrupted * @throws ExecutionException If the data transfer process failed */ public String initiateTransferProcess(URL providerUrl, String agreementId, String assetId, - URL dataDestinationUrl) throws InterruptedException, ExecutionException { + URL dataDestinationUrl) throws InterruptedException, ExecutionException { // Prepare for incoming data var dataFuture = new CompletableFuture(); dataTransferObservable.register(dataFuture, agreementId); if (Objects.isNull(dataDestinationUrl)) { var apiKey = UUID.randomUUID().toString(); - dataEndpointAuthenticationRequestFilter.addTemporaryApiKey(DATA_TRANSFER_API_KEY, apiKey); + dataTransferEndpointManager.addTemporaryEndpoint(agreementId, DATA_TRANSFER_API_KEY, apiKey); this.transferInitiator.initiateTransferProcess(providerUrl, agreementId, assetId, apiKey); return waitForData(dataFuture, agreementId); @@ -112,7 +108,7 @@ public String initiateTransferProcess(URL providerUrl, String agreementId, Strin private String waitForData(CompletableFuture dataFuture, String agreementId) throws InterruptedException, ExecutionException { - var waitForTransferTimeout = config.getInteger("getWaitForTransferTimeout", + var waitForTransferTimeout = config.getInteger("waitForTransferTimeout", WAIT_FOR_TRANSFER_TIMEOUT_DEFAULT); try { // Fetch TransferTimeout everytime to adapt to runtime config changes diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiator.java b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiator.java index 72c149db..1129d26f 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiator.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiator.java @@ -15,16 +15,9 @@ */ package de.fraunhofer.iosb.client.dataTransfer; -import static de.fraunhofer.iosb.client.dataTransfer.DataTransferController.DATA_TRANSFER_API_KEY; -import static java.lang.String.format; -import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; -import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; - -import java.net.URI; -import java.net.URL; -import java.util.Objects; -import java.util.UUID; - +import de.fraunhofer.iosb.client.ClientEndpoint; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.UriBuilderException; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; import org.eclipse.edc.connector.transfer.spi.types.TransferRequest; import org.eclipse.edc.spi.EdcException; @@ -32,9 +25,16 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.domain.DataAddress; -import de.fraunhofer.iosb.client.ClientEndpoint; -import jakarta.ws.rs.core.UriBuilder; -import jakarta.ws.rs.core.UriBuilderException; +import java.net.URI; +import java.net.URL; +import java.util.Objects; +import java.util.UUID; + +import static de.fraunhofer.iosb.client.dataTransfer.DataTransferController.DATA_TRANSFER_API_KEY; +import static java.lang.String.format; +import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; + /** * Initiate transfer requests @@ -44,12 +44,14 @@ class TransferInitiator { private final TransferProcessManager transferProcessManager; private final Monitor monitor; private final URI ownUri; + private final String connectorId; TransferInitiator(Config config, Monitor monitor, - TransferProcessManager transferProcessManager) { + TransferProcessManager transferProcessManager, String connectorId) { this.monitor = monitor; this.ownUri = createOwnUriFromConfigurationValues(config); this.transferProcessManager = transferProcessManager; + this.connectorId = connectorId; } void initiateTransferProcess(URL providerUrl, String agreementId, String assetId, String apiKey) { @@ -73,8 +75,9 @@ void initiateTransferProcess(URL providerUrl, String agreementId, String assetId var transferRequest = TransferRequest.Builder.newInstance() .id(UUID.randomUUID().toString()) // this is not relevant, thus can be random .connectorId(providerUrl.toString()) // the address of the provider connector + .counterPartyAddress(providerUrl.toString()) .protocol(DATASPACE_PROTOCOL_HTTP) - .connectorId("consumer") + .connectorId(this.connectorId) .assetId(assetId) .dataDestination(dataSinkAddress) .contractId(agreementId) @@ -87,9 +90,22 @@ void initiateTransferProcess(URL providerUrl, String agreementId, String assetId } private URI createOwnUriFromConfigurationValues(Config config) { - var protocolAddressString = config.getString("edc.dsp.callback.address", null); - var ownPort = config.getInteger("web.http.port", -1); - var ownPath = config.getString("web.http.path", null); + String protocolAddressString; + int ownPort; + String ownPath; + try { + protocolAddressString = config.getString("edc.dsp.callback.address"); + ownPort = config.getInteger("web.http.port", -1); + ownPath = config.getString("web.http.path", null); + } catch (EdcException noSettingFound) { + monitor.severe( + format("[Client] Could not build own URI, thus cannot transfer data to this EDC. Only data transfers to external endpoints are supported. Exception message: %s", + noSettingFound.getMessage())); + return null; + } + + // Remove /dsp from URL + protocolAddressString = protocolAddressString.substring(0, protocolAddressString.length() - "/dsp".length()); try { return UriBuilder .fromUri(protocolAddressString) @@ -104,7 +120,7 @@ private URI createOwnUriFromConfigurationValues(Config config) { } catch (IllegalArgumentException | UriBuilderException ownUriBuilderException) { monitor.severe( format("[Client] Could not build own URI, thus cannot transfer data to this EDC. Only data transfers to external endpoints are supported. Exception message: %s", - ownUriBuilderException.getMessage())); + ownUriBuilderException.getMessage())); } return null; } diff --git a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/Negotiator.java b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/Negotiator.java index fbf28e3c..dcc165bd 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/Negotiator.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/Negotiator.java @@ -37,8 +37,6 @@ public class Negotiator { * Class constructor * * @param consumerNegotiationManager Initiating a negotiation as a consumer. - * @param observable Status updates for waiting data transfer - * requesters to avoid busy waiting. * @param contractNegotiationStore Check for existing agreements before * negotiating */ diff --git a/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java b/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java index 60db702d..e877a5ea 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java @@ -32,6 +32,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; +import de.fraunhofer.iosb.api.PublicApiManagementService; import org.eclipse.edc.api.auth.spi.AuthenticationService; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.Dataset; @@ -116,8 +117,8 @@ public void setup() throws IOException { mock(Monitor.class), mockConfig(), mock(WebService.class), - mock(AuthenticationService.class), - mockTransferProcessManager())); + mock(PublicApiManagementService.class), + mockTransferProcessManager(), "")); } private Config mockConfig() { @@ -219,7 +220,7 @@ public void getAcceptedContractOffersTest() { public void addAcceptedContractOffersTest() { var mockPolicyDefinitionsAsList = new ArrayList(); mockPolicyDefinitionsAsList.add(mockPolicyDefinition); // ClientEndpoint creates ArrayList - var offers = new PolicyDefinition[] { mockPolicyDefinition }; + var offers = new PolicyDefinition[]{mockPolicyDefinition}; clientEndpoint.addAcceptedPolicyDefinitions(offers); @@ -228,7 +229,7 @@ public void addAcceptedContractOffersTest() { @Test public void updateAcceptedContractOfferTest() { - var offers = new PolicyDefinition[] { mockPolicyDefinition }; + var offers = new PolicyDefinition[]{mockPolicyDefinition}; clientEndpoint.addAcceptedPolicyDefinitions(offers); diff --git a/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java b/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java index 32733b50..0893a4c6 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java @@ -1,8 +1,5 @@ package de.fraunhofer.iosb.client; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - import org.eclipse.edc.api.auth.spi.AuthenticationService; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; @@ -18,6 +15,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.Mockito.*; + @ExtendWith(DependencyInjectionExtension.class) public class ClientExtensionTest { @@ -37,6 +36,7 @@ void setup(ServiceExtensionContext context, ObjectFactory factory) { context.registerService(Monitor.class, mock(Monitor.class)); this.context = spy(context); + clientExtension = factory.constructInstance(ClientExtension.class); } diff --git a/client/src/test/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiatorTest.java b/client/src/test/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiatorTest.java index 75a65689..ea5253aa 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiatorTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiatorTest.java @@ -52,7 +52,7 @@ void initializeContractOfferService() { var configMock = ConfigFactory.fromMap(Map.of("edc.dsp.callback.address", "http://localhost:4321/dsp", "web.http.port", "8080", "web.http.path", "/api")); - transferInitiator = new TransferInitiator(configMock, mock(Monitor.class), mockTransferProcessManager); + transferInitiator = new TransferInitiator(configMock, mock(Monitor.class), mockTransferProcessManager, "http://localhost"); mockStatusResult = (StatusResult) mock(StatusResult.class); diff --git a/edc-extension4aas/build.gradle.kts b/edc-extension4aas/build.gradle.kts index aa509516..e78c0621 100644 --- a/edc-extension4aas/build.gradle.kts +++ b/edc-extension4aas/build.gradle.kts @@ -18,6 +18,9 @@ java { } dependencies { + // Centralized auth request filter + implementation(project(":public-api-management")) + // See this project's README.MD for explanations implementation("$group:contract-core:$edcVersion") implementation("$group:management-api:$edcVersion") diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/AasExtension.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/AasExtension.java index 33ae556b..bd3d92d0 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/AasExtension.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/AasExtension.java @@ -15,13 +15,15 @@ */ package de.fraunhofer.iosb.app; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Objects; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - +import de.fraunhofer.iosb.api.PublicApiManagementService; +import de.fraunhofer.iosb.api.model.HttpMethod; +import de.fraunhofer.iosb.app.controller.AasController; +import de.fraunhofer.iosb.app.controller.ConfigurationController; +import de.fraunhofer.iosb.app.controller.ResourceController; +import de.fraunhofer.iosb.app.model.configuration.Configuration; +import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; +import de.fraunhofer.iosb.app.sync.Synchronizer; +import okhttp3.OkHttpClient; import org.eclipse.edc.api.auth.spi.AuthenticationService; import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; @@ -31,20 +33,24 @@ import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; -import de.fraunhofer.iosb.app.authentication.CustomAuthenticationRequestFilter; -import de.fraunhofer.iosb.app.controller.AasController; -import de.fraunhofer.iosb.app.controller.ConfigurationController; -import de.fraunhofer.iosb.app.controller.ResourceController; -import de.fraunhofer.iosb.app.model.configuration.Configuration; -import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; -import de.fraunhofer.iosb.app.sync.Synchronizer; -import okhttp3.OkHttpClient; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + /** * EDC Extension supporting usage of Asset Administration Shells. */ public class AasExtension implements ServiceExtension { + // Non-public unified authentication request filter management service + @Inject + private PublicApiManagementService publicApiManagementService; + @Inject private AssetIndex assetIndex; @Inject @@ -59,7 +65,7 @@ public class AasExtension implements ServiceExtension { private WebService webService; private static final String SETTINGS_PREFIX = "edc.aas"; - private static final Logger logger = Logger.getInstance(); + private static final Logger LOGGER = Logger.getInstance(); private final ScheduledExecutorService syncExecutor = new ScheduledThreadPoolExecutor(1); private AasController aasController; @@ -76,10 +82,11 @@ public void initialize(ServiceExtensionContext context) { initializeSynchronizer(selfDescriptionRepository); registerServicesByConfig(selfDescriptionRepository); - var authenticationRequestFilter = new CustomAuthenticationRequestFilter(authenticationService, - Configuration.getInstance().isExposeSelfDescription() ? Endpoint.SELF_DESCRIPTION_PATH : null); + // Add public endpoint if wanted by config + if (Configuration.getInstance().isExposeSelfDescription()) { + publicApiManagementService.addEndpoints(List.of(new de.fraunhofer.iosb.api.model.Endpoint(Endpoint.SELF_DESCRIPTION_PATH, HttpMethod.GET, null))); + } - webService.registerResource(authenticationRequestFilter); webService.registerResource(endpoint); } @@ -103,7 +110,7 @@ private void registerServicesByConfig(SelfDescriptionRepository selfDescriptionR selfDescriptionRepository.createSelfDescription(serviceUrl); } catch (IOException startAASException) { - logger.warning("Could not start AAS service provided by configuration", startAASException); + LOGGER.warning("Could not start AAS service provided by configuration", startAASException); } } @@ -121,7 +128,7 @@ private void initializeSynchronizer(SelfDescriptionRepository selfDescriptionRep @Override public void shutdown() { - logger.info("Shutting down EDC4AAS extension..."); + LOGGER.info("Shutting down EDC4AAS extension..."); syncExecutor.shutdown(); aasController.stopServices(); } diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilter.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilter.java deleted file mode 100644 index 818b6f61..00000000 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilter.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige - * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten - * Forschung e.V. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.fraunhofer.iosb.app.authentication; - -import java.util.Objects; - -import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter; -import org.eclipse.edc.api.auth.spi.AuthenticationService; - -import de.fraunhofer.iosb.app.Logger; -import jakarta.ws.rs.container.ContainerRequestContext; - -/** - * Custom AuthenticationRequestFilter filtering requests that go directly to an - * AAS service (managed by this extension) or the extension's configuration. - */ -public class CustomAuthenticationRequestFilter extends AuthenticationRequestFilter { - - private static final Logger LOGGER = Logger.getInstance(); - private final String[] endpoints; - - public CustomAuthenticationRequestFilter(AuthenticationService authenticationService, String... acceptedEndpoints) { - super(authenticationService); - if (Objects.nonNull(acceptedEndpoints)) { - endpoints = acceptedEndpoints; - } else { - endpoints = new String[0]; - } - } - - /** - * On automated data transfer: If the request is valid, the key,value pair used - * for this request will no longer be valid. - */ - @Override - public void filter(ContainerRequestContext requestContext) { - Objects.requireNonNull(requestContext); - var requestPath = requestContext.getUriInfo().getPath(); - - for (String endpoint : endpoints) { - if (Objects.nonNull(endpoint) && endpoint.equalsIgnoreCase(requestPath)) { - LOGGER.debug( - "CustomAuthenticationRequestFilter: Not intercepting this request to an open endpoint"); - return; - } - } - - super.filter(requestContext); - } -} diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilterTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilterTest.java deleted file mode 100644 index 56635dd1..00000000 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilterTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige - * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten - * Forschung e.V. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.fraunhofer.iosb.app.authentication; - -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.Objects; - -import org.eclipse.edc.api.auth.spi.AuthenticationService; -import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import de.fraunhofer.iosb.app.Endpoint; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.MultivaluedHashMap; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.UriInfo; - -public class CustomAuthenticationRequestFilterTest { - - final AuthenticationService authService = mock(AuthenticationService.class); - CustomAuthenticationRequestFilter authRequestFilter; - - @BeforeEach - public void initializeTestObject() { - authRequestFilter = new CustomAuthenticationRequestFilter(authService, "selfDescription"); - } - - @Test - void filterDataTransferTest() { - - verify(authService, times(0)).isAuthenticated(any()); - - var mockedContext = createSemiAuthenticRequestContext(Endpoint.SELF_DESCRIPTION_PATH, false, - new MultivaluedHashMap<>(Map.of("test-key", "test-password"))); - authRequestFilter.filter(mockedContext); - - } - - @Test - void filterSelfDescriptionTest() { - verify(authService, times(0)).isAuthenticated(any()); - - var mockedContext = createSemiAuthenticRequestContext(Endpoint.SELF_DESCRIPTION_PATH, false); - authRequestFilter.filter(mockedContext); - } - - @Test - void filterRequestUnauthenticatedTest() { - var mockedContext = createSemiAuthenticRequestContext("config", false); - - try { - authRequestFilter.filter(mockedContext); - fail(); - } catch (AuthenticationFailedException expected) { - } - } - - @Test - void filterRequestAuthenticatedTest() { - var mockedContext = createSemiAuthenticRequestContext("unauthorizedPath", true); - authRequestFilter.filter(mockedContext); - } - - private ContainerRequestContext createSemiAuthenticRequestContext(String returnedPath, - boolean isAuthenticatedMockResponse) { - return createSemiAuthenticRequestContext(returnedPath, isAuthenticatedMockResponse, null); - } - - /* - * Just enough parameters are mocked so that the super class filter method does - * not crash - */ - private ContainerRequestContext createSemiAuthenticRequestContext(String returnedPath, - boolean isAuthenticatedMockResponse, - MultivaluedMap additionalHeaders) { - ContainerRequestContext mockedContainerRequestContext = mock(ContainerRequestContext.class); - UriInfo mockedUriInfo = mock(UriInfo.class); - when(mockedUriInfo.getPath()).thenReturn(returnedPath); - - when(mockedContainerRequestContext.getUriInfo()).thenReturn(mockedUriInfo); - - // Super class needs these to not crash - when(mockedContainerRequestContext.getHeaders()) - .thenReturn(Objects.nonNull(additionalHeaders) ? additionalHeaders : new MultivaluedHashMap<>()); - when(mockedContainerRequestContext.getMethod()).thenReturn("POST"); - setAuthenticatedBySuperclass(isAuthenticatedMockResponse); - return mockedContainerRequestContext; - } - - private void setAuthenticatedBySuperclass(boolean authenticated) { - when(authService.isAuthenticated(any())).thenReturn(authenticated); - } -} diff --git a/example/dataspaceconnector-configuration.properties b/example/dataspaceconnector-configuration.properties index 9c93d224..1dd5344a 100644 --- a/example/dataspaceconnector-configuration.properties +++ b/example/dataspaceconnector-configuration.properties @@ -44,5 +44,5 @@ edc.hostname=localhost edc.api.auth.key=password edc.jsonld.https.enabled=true -edc.dsp.id=provider -edc.participant.id=provider \ No newline at end of file +edc.dsp.id=consumer +edc.participant.id=consumer \ No newline at end of file diff --git a/public-api-management/build.gradle.kts b/public-api-management/build.gradle.kts new file mode 100644 index 00000000..f2f0c984 --- /dev/null +++ b/public-api-management/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + `java-library` + jacoco +} + +val javaVersion: String by project +val edcVersion: String by project +val rsApi: String by project +val mockitoVersion: String by project +val mockserverVersion: String by project + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(javaVersion)) + } +} + +dependencies { + // See this project's README.MD for explanations + implementation("jakarta.ws.rs:jakarta.ws.rs-api:${rsApi}") + implementation("$group:auth-spi:$edcVersion") + + testImplementation("$group:junit:$edcVersion") + testImplementation("org.glassfish.jersey.core:jersey-common:3.1.3") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("org.mock-server:mockserver-junit-jupiter:${mockserverVersion}") + testImplementation("org.mock-server:mockserver-netty:${mockserverVersion}") +} + +repositories { + mavenCentral() +} + +tasks.test { + useJUnitPlatform() +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report +} diff --git a/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementExtension.java b/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementExtension.java new file mode 100644 index 00000000..1e8825bc --- /dev/null +++ b/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementExtension.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api; + +import de.fraunhofer.iosb.api.filter.CustomAuthenticationRequestFilter; +import org.eclipse.edc.api.auth.spi.AuthenticationService; +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.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +/** + * Manage public api endpoints in a unified extension. + * This is due to multiple independent authentication request filters + * not working properly, since they cannot "let a request through" + * to their endpoints without other registered authentication request filters + * accepting the request too. + */ +@Provides(PublicApiManagementService.class) +@Extension(value = PublicApiManagementExtension.NAME) +public class PublicApiManagementExtension implements ServiceExtension { + + public static final String NAME = "Public API Endpoint Management"; + + // Our authentication request filter needs this service to work: + @Inject + private AuthenticationService authenticationService; + // To register our authentication request filter, we need: + @Inject + private WebService webService; + + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + var filter = new CustomAuthenticationRequestFilter(authenticationService, monitor); + + webService.registerResource(filter); + + // Register this service to be accessible by other extensions + context.registerService(PublicApiManagementService.class, new PublicApiManagementService(filter, monitor)); + } +} diff --git a/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementService.java b/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementService.java new file mode 100644 index 00000000..3889941a --- /dev/null +++ b/public-api-management/src/main/java/de/fraunhofer/iosb/api/PublicApiManagementService.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api; + +import de.fraunhofer.iosb.api.filter.CustomAuthenticationRequestFilter; +import de.fraunhofer.iosb.api.model.Endpoint; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +/** + * Let other extensions add public endpoints. + */ +public class PublicApiManagementService { + + private final CustomAuthenticationRequestFilter filter; + private final Monitor monitor; + + public PublicApiManagementService(CustomAuthenticationRequestFilter filter, Monitor monitor) { + this.filter = filter; + this.monitor = monitor; + } + + /** + * Add a collection of public endpoints for the request filter to accept. + * + * @param endpoints Non-null collection of endpoints. + */ + public void addEndpoints(Collection endpoints) { + Objects.requireNonNull(endpoints, "endpoints must not be null"); + monitor.info(format("PublicApiManagementService: Adding %s public endpoints to filter.", endpoints.size())); + var nonNullEndpoints = endpoints.stream().filter(Objects::nonNull).collect(Collectors.toList()); + filter.addEndpoints(nonNullEndpoints); + } + + /** + * Add a temporary public endpoint for the request filter to accept. + * + * @param endpoint Non-null endpoint. + */ + public void addTemporaryEndpoint(Endpoint endpoint) { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + monitor.info(format("PublicApiManagementService: Adding public endpoint %s to filter", endpoint.suffix())); + filter.addTemporaryEndpoint(endpoint); + } + + /** + * Remove a collection of public endpoints for the request filter to accept. + * + * @param endpoints Non-null collection of endpoints. + */ + public void removeEndpoints(Collection endpoints) { + Objects.requireNonNull(endpoints, "endpoints must not be null"); + monitor.info(format("PublicApiManagementService: Removing %s public endpoints from filter.", endpoints.size())); + filter.removeEndpoints(endpoints); + } +} diff --git a/public-api-management/src/main/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilter.java b/public-api-management/src/main/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilter.java new file mode 100644 index 00000000..6890808a --- /dev/null +++ b/public-api-management/src/main/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilter.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api.filter; + +import de.fraunhofer.iosb.api.model.Endpoint; +import de.fraunhofer.iosb.api.model.HttpMethod; +import jakarta.ws.rs.container.ContainerRequestContext; +import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter; +import org.eclipse.edc.api.auth.spi.AuthenticationService; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +/** + * Custom AuthenticationRequestFilter filtering requests that go directly to public endpoints. + * Endpoints can be made public by adding them to this filter's list. + */ +public class CustomAuthenticationRequestFilter extends AuthenticationRequestFilter { + + private final Monitor monitor; + private final Collection endpoints; + private final Collection temporaryEndpoints; + + public CustomAuthenticationRequestFilter(AuthenticationService authenticationService, Monitor monitor) { + super(authenticationService); + this.monitor = monitor; + endpoints = new ArrayList<>(); + temporaryEndpoints = new ArrayList<>(); + } + + /** + * On automated data transfer: If the request is valid, the key,value pair used + * for this request will no longer be valid. + */ + @Override + public void filter(ContainerRequestContext requestContext) { + Objects.requireNonNull(requestContext); + var requestedEndpoint = parseEndpoint(requestContext); + for (Endpoint endpoint : endpoints) { + if (endpoint.isCoveredBy(requestedEndpoint)) { + monitor.debug(format("CustomAuthenticationRequestFilter: Accepting request to open endpoint %s", endpoint.suffix())); + return; + } + } + for (Endpoint endpoint : temporaryEndpoints) { + if (endpoint.isCoveredBy(requestedEndpoint)) { + monitor.debug(format("CustomAuthenticationRequestFilter: Accepting request to open temporary endpoint %s", endpoint.suffix())); + temporaryEndpoints.remove(endpoint); + return; + } + } + + super.filter(requestContext); + } + + private Endpoint parseEndpoint(ContainerRequestContext requestContext) { + var requestPath = requestContext.getUriInfo().getPath(); + var method = HttpMethod.valueOf(requestContext.getMethod()); + var headers = requestContext.getHeaders().entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + + return new Endpoint(requestPath, method, headers); + } + + public boolean addEndpoints(Collection endpoints) { + var newEndpoints = endpoints.stream().filter(newEndpoint -> !this.endpoints.contains(newEndpoint)).toList(); + return this.endpoints.addAll(newEndpoints); + } + + public boolean removeEndpoints(Collection endpoints) { + return this.endpoints.removeAll(endpoints); + } + + public void addTemporaryEndpoint(Endpoint endpoint) { + temporaryEndpoints.add(endpoint); + } +} diff --git a/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/Endpoint.java b/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/Endpoint.java new file mode 100644 index 00000000..9387ca61 --- /dev/null +++ b/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/Endpoint.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api.model; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Record of an endpoint. + * + * @param suffix The relevant suffix of the endpoint (i.e. the path of the URL). + * @param method The method through which the endpoint can be accessed. + * @param customHeaders Custom headers like special authentication keys can be passed here. This is a multivalued map + */ +public record Endpoint(String suffix, HttpMethod method, Map> customHeaders) { + + public Endpoint { + Objects.requireNonNull(suffix); + Objects.requireNonNull(method); + } + + /** + * Check whether this endpoint instance is covered by the other endpoint. + * This means, suffix and method have to match. + * Headers of this endpoint have to be within other's headers. + * This is to check if the other endpoint contains custom needed headers like additional api keys. + * + * @param other Other endpoint, whose headers must contain this endpoint's headers, as well as match suffix and method. + * @return True if the condition holds, else false. + */ + public boolean isCoveredBy(Endpoint other) { + if (!this.suffix().equals(other.suffix()) || !this.method().equals(other.method())) { + return false; + } + return this.customHeaders().entrySet().stream().allMatch(entry -> + other.customHeaders().containsKey(entry.getKey()) && + new HashSet<>(other.customHeaders().get(entry.getKey())).containsAll(entry.getValue())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Endpoint endpoint = (Endpoint) o; + return Objects.equals(suffix, endpoint.suffix) && method == endpoint.method && Objects.equals(customHeaders, endpoint.customHeaders); + } + + @Override + public int hashCode() { + return Objects.hash(suffix, method, customHeaders); + } +} diff --git a/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/HttpMethod.java b/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/HttpMethod.java new file mode 100644 index 00000000..9c3ba471 --- /dev/null +++ b/public-api-management/src/main/java/de/fraunhofer/iosb/api/model/HttpMethod.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api.model; + +/** + * Http methods for a typesafe use of endpoint class + */ +public enum HttpMethod { + /** + * HTTP GET + */ + GET, + /** + * HTTP POST + */ + POST, + /** + * HTTP PUT + */ + PUT, + /** + * HTTP DELETE + */ + DELETE, + /** + * HTTP PATCH + */ + PATCH, + /** + * HTTP HEAD + */ + HEAD, + /** + * HTTP OPTIONS + */ + OPTIONS +} diff --git a/public-api-management/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/public-api-management/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000..69d42fc7 --- /dev/null +++ b/public-api-management/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.fraunhofer.iosb.api.PublicApiManagementExtension \ No newline at end of file diff --git a/public-api-management/src/test/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilterTest.java b/public-api-management/src/test/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilterTest.java new file mode 100644 index 00000000..55a4b4c1 --- /dev/null +++ b/public-api-management/src/test/java/de/fraunhofer/iosb/api/filter/CustomAuthenticationRequestFilterTest.java @@ -0,0 +1,132 @@ +package de.fraunhofer.iosb.api.filter; + +import de.fraunhofer.iosb.api.model.Endpoint; +import de.fraunhofer.iosb.api.model.HttpMethod; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.UriInfo; +import org.eclipse.edc.api.auth.spi.AuthenticationService; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static de.fraunhofer.iosb.api.model.EndpointTest.createNormalEndpoint; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CustomAuthenticationRequestFilterTest { + + CustomAuthenticationRequestFilter customAuthenticationRequestFilter; + AuthenticationService mockAuthenticationService; + + @BeforeEach + void setUp() { + mockAuthenticationService = mock(AuthenticationService.class); + customAuthenticationRequestFilter = new CustomAuthenticationRequestFilter(mockAuthenticationService, mock(Monitor.class)); + } + + @SuppressWarnings("unchecked") + @Test + void filterGoThrough() { + // Make a request that should be accepted by our filter. + // First, prepare the filter by adding an endpoint. + var headers = Map.of("y-api-key", List.of("pasword")); + customAuthenticationRequestFilter.addEndpoints(List.of(new Endpoint("/api/suffix/test", HttpMethod.DELETE, headers))); + // Create the mock request + var mockRequest = mock(ContainerRequestContext.class); + var mockUriInfo = mock(UriInfo.class); + when(mockUriInfo.getPath()).thenReturn("/api/suffix/test"); + when(mockRequest.getUriInfo()).thenReturn(mockUriInfo); + when(mockRequest.getMethod()).thenReturn("DELETE"); + var mockMultiValueMap = mock(MultivaluedMap.class); + when(mockMultiValueMap.entrySet()).thenReturn(headers.entrySet()); + when(mockRequest.getHeaders()).thenReturn(mockMultiValueMap); + + // This should not be called + when(mockAuthenticationService.isAuthenticated(any())).thenThrow(IllegalAccessError.class); + + // This should run through + customAuthenticationRequestFilter.filter(mockRequest); + } + + @SuppressWarnings("unchecked") + @Test + void filterWrongHttpMethod() { + // Make a request that should not be accepted by our filter. + // First, prepare the filter by adding an endpoint. + var headers = Map.of("y-api-key", List.of("pasword")); + customAuthenticationRequestFilter.addEndpoints(List.of(new Endpoint("/api/suffix/test", HttpMethod.PATCH, headers))); + // Create the mock request + var mockRequest = mock(ContainerRequestContext.class); + var mockUriInfo = mock(UriInfo.class); + when(mockUriInfo.getPath()).thenReturn("/api/suffix/test"); + when(mockRequest.getUriInfo()).thenReturn(mockUriInfo); + when(mockRequest.getMethod()).thenReturn("DELETE"); + var mockMultiValueMap = mock(MultivaluedMap.class); + when(mockMultiValueMap.entrySet()).thenReturn(headers.entrySet()); + when(mockRequest.getHeaders()).thenReturn(mockMultiValueMap); + + + try { + // This should delegate request to superclass + customAuthenticationRequestFilter.filter(mockRequest); + fail(); + } catch (AuthenticationFailedException expected) { + } + // This should be called once + verify(mockAuthenticationService, times(1)).isAuthenticated(any()); + } + + @Test + void addEndpoints() { + // Pre: No endpoints + // We only test if adding breaks + var endpoints = new ArrayList<>(List.of(createNormalEndpoint(), createNormalEndpoint(), createNormalEndpoint())); + assertTrue(customAuthenticationRequestFilter.addEndpoints(endpoints)); + } + + @SuppressWarnings("unchecked") + @Test + void addSameEndpointsUnchanged() { + var endpoints = new ArrayList<>(List.of(createNormalEndpoint(), createNormalEndpoint(), createNormalEndpoint())); + customAuthenticationRequestFilter.addEndpoints(endpoints); + + // Pre: Three endpoints + // Test if adding the same endpoint(s) changes the state + + ArrayList sameEndpoints = (ArrayList) endpoints.clone(); + assertFalse(customAuthenticationRequestFilter.addEndpoints(sameEndpoints)); + } + + @Test + void removeEndpoints() { + var endpoints = new ArrayList<>(List.of(createNormalEndpoint(), createNormalEndpoint(), createNormalEndpoint())); + customAuthenticationRequestFilter.addEndpoints(endpoints); + + // Pre: Three endpoints + // Test if removing these same endpoints changes the state + var sameEndpoints = cloneEndpoints(endpoints); + assertTrue(customAuthenticationRequestFilter.removeEndpoints(sameEndpoints)); + + // Now all endpoints should be removed -> removing again should not change state + sameEndpoints = cloneEndpoints(endpoints); + assertFalse(customAuthenticationRequestFilter.removeEndpoints(sameEndpoints)); + } + + private ArrayList cloneEndpoints(ArrayList endpointCollection) { + var sameEndpoints = new ArrayList<>(endpointCollection); + sameEndpoints.forEach(oldEndpoint -> new Endpoint(oldEndpoint.suffix(), oldEndpoint.method(), oldEndpoint.customHeaders())); + return sameEndpoints; + } +} \ No newline at end of file diff --git a/public-api-management/src/test/java/de/fraunhofer/iosb/api/model/EndpointTest.java b/public-api-management/src/test/java/de/fraunhofer/iosb/api/model/EndpointTest.java new file mode 100644 index 00000000..4935970a --- /dev/null +++ b/public-api-management/src/test/java/de/fraunhofer/iosb/api/model/EndpointTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.api.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class EndpointTest { + + Endpoint endpoint; + + public static Endpoint createNormalEndpoint() { + return new Endpoint("/api/suffix", HttpMethod.HEAD, + Map.of("api-key-super-secret", List.of("12345678"))); + } + + @Test + void isCoveredByMoreValuesInList() { + endpoint = createNormalEndpoint(); + + var coveringEndpoint = new Endpoint("/api/suffix", HttpMethod.HEAD, + Map.of("api-key-super-secret", List.of("12345678", "another-passkey-wow"))); + assertTrue(endpoint.isCoveredBy(coveringEndpoint)); + } + + @Test + void isCoveredByMoreValuesInMap() { + endpoint = createNormalEndpoint(); + + var coveringEndpoint = new Endpoint("/api/suffix", HttpMethod.HEAD, + Map.of("api-key-super-secret", List.of("12345678", "another-passkey-wow"), + "nother-key", List.of("Wowie!"))); + + assertTrue(endpoint.isCoveredBy(coveringEndpoint)); + } + + @Test + void isCoveredByEqualEndpoint() { + endpoint = createNormalEndpoint(); + + var coveringEndpoint = createNormalEndpoint(); + + assertTrue(endpoint.isCoveredBy(coveringEndpoint)); + } + + @Test + void isCoveredByFailOtherValue() { + endpoint = createNormalEndpoint(); + + var coveringEndpoint = new Endpoint("/api/suffix", HttpMethod.HEAD, + Map.of("api-key-super-secret", List.of("87654321"))); + + assertFalse(endpoint.isCoveredBy(coveringEndpoint)); + } + + @Test + void isCoveredByFailOtherKey() { + endpoint = createNormalEndpoint(); + + var coveringEndpoint = new Endpoint("/api/suffix", HttpMethod.HEAD, + Map.of("api-key-super-secret2", List.of("12345678"))); + + assertFalse(endpoint.isCoveredBy(coveringEndpoint)); + } + + @Test + void createWithNullValues() { + endpoint = createNormalEndpoint(); + + try { + new Endpoint(null, HttpMethod.HEAD, Map.of("", List.of(""))); + fail(); + } catch (NullPointerException expected) { + } + + } + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index dfb7d8df..3a89cda6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,5 @@ include(":edc-extension4aas") include(":client") // include the launcher in the build process -include(":example") \ No newline at end of file +include(":example") +include("public-api-management")