From 34f4ca715cbc11e3ead10d61bf4964ede0aa2558 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 24 Feb 2024 16:56:28 +0100 Subject: [PATCH 01/24] Update to EDC v0.5.1 This introduces counterPartyId to our requests which must be added to all client requests now, somehow the url alone does not suffice to build a catalog request, the requested EDC also needs its own identifier now to answer the request. --- .../iosb/client/ClientEndpoint.java | 104 +++++----- .../iosb/client/ClientExtension.java | 63 +++--- .../CustomAuthenticationRequestFilter.java | 2 +- .../dataTransfer/DataTransferController.java | 26 ++- .../dataTransfer/DataTransferEndpoint.java | 7 +- .../dataTransfer/DataTransferObservable.java | 4 +- .../dataTransfer/TransferInitiator.java | 32 ++- .../negotiation/NegotiationController.java | 18 +- .../iosb/client/negotiation/Negotiator.java | 8 +- .../iosb/client/policy/PolicyController.java | 24 +-- .../client/policy/PolicyDefinitionStore.java | 4 +- .../iosb/client/policy/PolicyService.java | 96 +++++---- .../iosb/client/ClientEndpointTest.java | 48 +++-- .../iosb/client/ClientExtensionTest.java | 6 +- .../dataTransfer/TransferInitiatorTest.java | 23 +-- .../client/negotiation/NegotiatorTest.java | 184 +++++++++--------- .../iosb/client/policy/PolicyServiceTest.java | 35 ++-- gradle.properties | 2 +- 18 files changed, 326 insertions(+), 360 deletions(-) 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..f909cef9 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java @@ -15,13 +15,13 @@ */ package de.fraunhofer.iosb.client; -import static java.lang.String.format; -import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; - -import java.net.URL; -import java.util.Objects; -import java.util.concurrent.ExecutionException; - +import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +import de.fraunhofer.iosb.client.negotiation.NegotiationController; +import de.fraunhofer.iosb.client.policy.PolicyController; +import de.fraunhofer.iosb.client.util.Pair; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; import org.eclipse.edc.policy.model.Policy; @@ -29,31 +29,21 @@ import org.eclipse.edc.spi.types.domain.agreement.ContractAgreement; import org.eclipse.edc.spi.types.domain.offer.ContractOffer; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; -import de.fraunhofer.iosb.client.negotiation.NegotiationController; -import de.fraunhofer.iosb.client.policy.PolicyController; -import de.fraunhofer.iosb.client.util.Pair; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import java.net.URL; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import static java.lang.String.format; +import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; /** * 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 { - /* - * Root path for the client - */ + public static final String AUTOMATED_PATH = "automated"; private static final String ACCEPTED_POLICIES_PATH = "acceptedPolicies"; private static final String DATASET_PATH = "dataset"; @@ -70,15 +60,14 @@ 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 policyController Finds out policy for a given asset id and provider EDC url. + * @param negotiationController Send contract offer, negotiation status watch. + * @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,17 +80,14 @@ 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) - public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryParam("assetId") String assetId) { + public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryParam("assetId") String assetId, @QueryParam("providerId") String counterPartyId) { monitor.debug(format("[Client] Received a %s GET request", DATASET_PATH)); if (Objects.isNull(providerUrl)) { @@ -109,7 +95,7 @@ public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryPar } try { - var dataset = policyController.getDataset(providerUrl, assetId); + var dataset = policyController.getDataset(counterPartyId, providerUrl, assetId); return Response.ok(dataset).build(); } catch (InterruptedException interruptedException) { monitor.severe(format("[Client] Getting dataset failed for provider %s and asset %s", providerUrl, @@ -123,28 +109,28 @@ 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 counterPartyUrl Provider EDCs URL (DSP endpoint) + * @param counterPartyId 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) { + public Response negotiateContract(@QueryParam("providerUrl") URL counterPartyUrl, + @QueryParam("providerId") String counterPartyId, + @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"); + Objects.requireNonNull(counterPartyUrl, "Provider URL must not be null"); + Objects.requireNonNull(counterPartyId, "Provider ID must not be null"); Objects.requireNonNull(assetId, "Asset ID must not be null"); Pair idPolicyPair; // id means contractOfferId try { - idPolicyPair = policyController.getAcceptablePolicyForAssetId(providerUrl, assetId); + idPolicyPair = policyController.getAcceptablePolicyForAssetId(counterPartyId, counterPartyUrl, assetId); } catch (InterruptedException negotiationException) { - monitor.severe(format("[Client] Getting policies failed for provider %s and asset %s", providerUrl, + monitor.severe(format("[Client] Getting policies failed for provider %s and asset %s", counterPartyUrl, assetId), negotiationException); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(negotiationException.getMessage()) .build(); @@ -158,8 +144,8 @@ public Response negotiateContract(@QueryParam("providerUrl") URL providerUrl, var contractRequest = ContractRequest.Builder.newInstance() .contractOffer(offer) - .counterPartyAddress(providerUrl.toString()) - .providerId(providerId) + .counterPartyAddress(counterPartyUrl.toString()) + .providerId(counterPartyId) .protocol(DATASPACE_PROTOCOL_HTTP) .build(); ContractAgreement agreement; @@ -167,13 +153,13 @@ public Response negotiateContract(@QueryParam("providerUrl") URL providerUrl, try { agreement = negotiationController.negotiateContract(contractRequest); } catch (InterruptedException | ExecutionException negotiationException) { - monitor.severe(format("[Client] Negotiation failed for provider %s and contractOffer %s", providerUrl, + monitor.severe(format("[Client] Negotiation failed for provider %s and contractOffer %s", counterPartyUrl, offer.getId()), negotiationException); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(negotiationException.getMessage()) .build(); } - return getData(providerUrl, agreement.getId(), assetId, dataDestinationUrl); + return getData(counterPartyUrl, agreement.getId(), assetId, dataDestinationUrl); } /** @@ -205,17 +191,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..45f744ec 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java @@ -15,6 +15,9 @@ */ package de.fraunhofer.iosb.client; +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.api.auth.spi.AuthenticationService; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; @@ -27,45 +30,41 @@ 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; + @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; - @Override - public void initialize(ServiceExtensionContext context) { - var monitor = context.getMonitor(); - var config = context.getConfig("edc.client"); + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + var config = context.getConfig("edc.client"); - var policyController = new PolicyController(monitor, catalogService, transformer, config); + var policyController = new PolicyController(monitor, catalogService, transformer, config); - var negotiationController = new NegotiationController(consumerNegotiationManager, - contractNegotiationObservable, contractNegotiationStore, config); + var negotiationController = new NegotiationController(consumerNegotiationManager, + contractNegotiationObservable, contractNegotiationStore, config); - var dataTransferController = new DataTransferController(monitor, config, webService, - authenticationService, transferProcessManager); + var dataTransferController = new DataTransferController(monitor, config, webService, + authenticationService, transferProcessManager); - webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, - dataTransferController)); + 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/CustomAuthenticationRequestFilter.java index 63b8a74e..01333a40 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java @@ -67,7 +67,7 @@ public void filter(ContainerRequestContext requestContext) { if (requestContext.getHeaders().containsKey(key) && requestContext.getHeaderString(key).equals(tempKeys.get(key)) && requestPath.startsWith( - format("%s/%s", ClientEndpoint.AUTOMATED_PATH, DataTransferEndpoint.RECEIVE_DATA_PATH))) { + 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); 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 16dc896b..4a9dbc51 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 @@ -15,7 +15,14 @@ */ package de.fraunhofer.iosb.client.dataTransfer; -import static java.lang.String.format; +import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; +import org.eclipse.edc.api.auth.spi.AuthenticationService; +import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; +import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.web.spi.WebService; import java.net.URL; import java.util.Objects; @@ -25,15 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.eclipse.edc.api.auth.spi.AuthenticationService; -import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; -import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.configuration.Config; -import org.eclipse.edc.web.spi.WebService; - -import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; +import static java.lang.String.format; public class DataTransferController { @@ -62,7 +61,7 @@ public class DataTransferController { * consumer. */ public DataTransferController(Monitor monitor, Config config, WebService webService, - AuthenticationService authenticationService, TransferProcessManager transferProcessManager) { + AuthenticationService authenticationService, TransferProcessManager transferProcessManager) { this.config = config; this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); this.dataEndpointAuthenticationRequestFilter = new CustomAuthenticationRequestFilter(monitor, @@ -82,14 +81,13 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ * @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. + * 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); diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java index 8b0cc590..3637ebe3 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java @@ -19,18 +19,17 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.spi.monitor.Monitor; import java.util.Objects; -import org.eclipse.edc.spi.monitor.Monitor; - import static java.lang.String.format; /** * Endpoint for automated data transfer */ -@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 DataTransferEndpoint { diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java index fcf6b1fe..d3ac513a 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java @@ -15,12 +15,12 @@ */ package de.fraunhofer.iosb.client.dataTransfer; +import org.eclipse.edc.spi.monitor.Monitor; + import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.edc.spi.monitor.Monitor; - import static java.lang.String.format; /** 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..b469abd2 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,15 @@ 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 @@ -46,7 +45,7 @@ class TransferInitiator { private final URI ownUri; TransferInitiator(Config config, Monitor monitor, - TransferProcessManager transferProcessManager) { + TransferProcessManager transferProcessManager) { this.monitor = monitor; this.ownUri = createOwnUriFromConfigurationValues(config); this.transferProcessManager = transferProcessManager; @@ -72,9 +71,8 @@ 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()) // the address of the provider connector .protocol(DATASPACE_PROTOCOL_HTTP) - .connectorId("consumer") .assetId(assetId) .dataDestination(dataSinkAddress) .contractId(agreementId) @@ -104,7 +102,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/NegotiationController.java b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java index 795094e6..0cfcd88d 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java @@ -15,13 +15,6 @@ */ package de.fraunhofer.iosb.client.negotiation; -import static java.lang.String.format; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - 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; @@ -31,6 +24,13 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.domain.agreement.ContractAgreement; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.lang.String.format; + /** * Provides API for contract negotiation by * {@link de.fraunhofer.iosb.client.negotiation.Negotiator the Negotiator @@ -48,8 +48,8 @@ public class NegotiationController { private final ClientContractNegotiationListener listener; public NegotiationController(ConsumerContractNegotiationManager consumerNegotiationManager, - ContractNegotiationObservable observable, ContractNegotiationStore contractNegotiationStore, - Config config) { + ContractNegotiationObservable observable, ContractNegotiationStore contractNegotiationStore, + Config config) { this.config = config; this.negotiator = new Negotiator(consumerNegotiationManager, contractNegotiationStore, config); this.listener = new ClientContractNegotiationListener(); 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 ae2018a2..f1b57f70 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 @@ -15,8 +15,6 @@ */ package de.fraunhofer.iosb.client.negotiation; -import java.util.concurrent.ExecutionException; - import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; @@ -25,6 +23,8 @@ import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.system.configuration.Config; +import java.util.concurrent.ExecutionException; + /** * Send contractrequest, negotiation status watch */ @@ -43,7 +43,7 @@ public class Negotiator { * negotiating */ public Negotiator(ConsumerContractNegotiationManager consumerNegotiationManager, - ContractNegotiationStore contractNegotiationStore, Config config) { + ContractNegotiationStore contractNegotiationStore, Config config) { this.consumerNegotiationManager = consumerNegotiationManager; this.contractNegotiationStore = contractNegotiationStore; } @@ -51,7 +51,7 @@ public Negotiator(ConsumerContractNegotiationManager consumerNegotiationManager, /* * InterruptedException: Thread for agreementId was waiting, sleeping, or * otherwise occupied, and was interrupted. - * + * * ExecutionException: Attempted to retrieve the agreementId but the task * aborted by throwing an exception. This exception can be inspected using the * getCause() method. diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java index 66608ba8..0a2cd034 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java @@ -15,10 +15,7 @@ */ package de.fraunhofer.iosb.client.policy; -import java.net.URL; -import java.util.List; -import java.util.Optional; - +import de.fraunhofer.iosb.client.util.Pair; import org.eclipse.edc.catalog.spi.Dataset; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; import org.eclipse.edc.connector.spi.catalog.CatalogService; @@ -27,7 +24,9 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import de.fraunhofer.iosb.client.util.Pair; +import java.net.URL; +import java.util.List; +import java.util.Optional; /** * Provides API for accepted policy management and provider dataset retrieval. @@ -42,7 +41,7 @@ public class PolicyController { private final PolicyService policyService; public PolicyController(Monitor monitor, CatalogService catalogService, - TypeTransformerRegistry typeTransformerRegistry, Config systemConfig) { + TypeTransformerRegistry typeTransformerRegistry, Config systemConfig) { this.config = new PolicyServiceConfig(systemConfig); this.policyDefinitionStore = new PolicyDefinitionStore(monitor, this.config.getAcceptedPolicyDefinitionsPath()); @@ -50,8 +49,8 @@ public PolicyController(Monitor monitor, CatalogService catalogService, this.policyDefinitionStore); } - public Dataset getDataset(URL providerUrl, String assetId) throws InterruptedException { - return policyService.getDatasetForAssetId(providerUrl, assetId); + public Dataset getDataset(String counterPartyId, URL counterPartyUrl, String assetId) throws InterruptedException { + return policyService.getDatasetForAssetId(counterPartyId, counterPartyUrl, assetId); } /** @@ -62,16 +61,17 @@ public Dataset getDataset(URL providerUrl, String assetId) throws InterruptedExc * 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. + * @param counterPartyId Provider of the asset. (id) + * @param counterPartyUrl Provider of the asset. (url) + * @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. */ - public Pair getAcceptablePolicyForAssetId(URL providerUrl, String assetId) + public Pair getAcceptablePolicyForAssetId(String counterPartyId, URL counterPartyUrl, String assetId) throws InterruptedException { - return policyService.getAcceptablePolicyForAssetId(providerUrl, assetId); + return policyService.getAcceptablePolicyForAssetId(counterPartyId, counterPartyUrl, assetId); } public void addAcceptedPolicyDefinitions(PolicyDefinition[] policyDefinitions) { diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java index 82de55ee..62dc25da 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java @@ -19,13 +19,13 @@ import org.eclipse.edc.connector.policy.spi.PolicyDefinition; import org.eclipse.edc.spi.monitor.Monitor; -import static java.lang.String.format; - import java.io.IOException; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import static java.lang.String.format; + /** * Contains user added PolicyDefinitions. */ diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java index 9cf0049a..4c191972 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java @@ -15,33 +15,13 @@ */ package de.fraunhofer.iosb.client.policy; -import static java.lang.String.format; -import static org.eclipse.edc.jsonld.spi.Namespaces.DCAT_PREFIX; -import static org.eclipse.edc.jsonld.spi.Namespaces.DCAT_SCHEMA; -import static org.eclipse.edc.jsonld.spi.Namespaces.DCT_PREFIX; -import static org.eclipse.edc.jsonld.spi.Namespaces.DCT_SCHEMA; -import static org.eclipse.edc.jsonld.spi.Namespaces.DSPACE_PREFIX; -import static org.eclipse.edc.jsonld.spi.Namespaces.DSPACE_SCHEMA; -import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCAT_ACCESS_SERVICE_ATTRIBUTE; -import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCT_FORMAT_ATTRIBUTE; -import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_PREFIX; -import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; -import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; -import static org.eclipse.edc.spi.query.Criterion.criterion; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URL; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import java.util.stream.Stream; - +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; +import de.fraunhofer.iosb.client.util.Pair; +import jakarta.json.Json; +import jakarta.json.JsonObject; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.DataService; import org.eclipse.edc.catalog.spi.Dataset; @@ -55,14 +35,27 @@ import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; -import de.fraunhofer.iosb.client.util.Pair; -import jakarta.json.Json; -import jakarta.json.JsonObject; +import static java.lang.String.format; +import static org.eclipse.edc.jsonld.spi.Namespaces.*; +import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCAT_ACCESS_SERVICE_ATTRIBUTE; +import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCT_FORMAT_ATTRIBUTE; +import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_PREFIX; +import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; +import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; +import static org.eclipse.edc.spi.query.Criterion.criterion; /** * Finds out policy for a given asset id and provider EDC url @@ -71,7 +64,7 @@ class PolicyService { private final CatalogService catalogService; private final TypeTransformerRegistry transformer; - + private final PolicyServiceConfig config; private final PolicyDefinitionStore policyDefinitionStore; @@ -82,7 +75,7 @@ class PolicyService { * @param transformer Transform json-ld byte-array catalog to catalog class */ public PolicyService(CatalogService catalogService, TypeTransformerRegistry transformer, - PolicyServiceConfig config, PolicyDefinitionStore policyDefinitionStore) { + PolicyServiceConfig config, PolicyDefinitionStore policyDefinitionStore) { this.catalogService = catalogService; this.transformer = transformer; @@ -90,9 +83,10 @@ public PolicyService(CatalogService catalogService, TypeTransformerRegistry tran this.policyDefinitionStore = policyDefinitionStore; } - Dataset getDatasetForAssetId(URL providerUrl, String assetId) throws InterruptedException { + Dataset getDatasetForAssetId(String counterPartyId, URL counterPartyUrl, String assetId) throws InterruptedException { var catalogFuture = catalogService.requestCatalog( - providerUrl.toString(), + counterPartyId, // why do we even need a provider id when we have the url... + counterPartyUrl.toString(), DATASPACE_PROTOCOL_HTTP, QuerySpec.Builder.newInstance() .filter(List.of(criterion(Asset.PROPERTY_ID, "=", assetId))) @@ -102,15 +96,15 @@ Dataset getDatasetForAssetId(URL providerUrl, String assetId) throws Interrupted try { catalogResponse = catalogFuture.get(config.getWaitForCatalogTimeout(), TimeUnit.SECONDS); } catch (ExecutionException futureExecutionException) { - throw new EdcException(format("Failed fetching a catalog by provider %s.", providerUrl), + throw new EdcException(format("Failed fetching a catalog by provider %s.", counterPartyUrl), futureExecutionException); } catch (TimeoutException timeoutCatalogFutureGetException) { - throw new EdcException(format("Timeout while waiting for catalog by provider %s.", providerUrl), + throw new EdcException(format("Timeout while waiting for catalog by provider %s.", counterPartyUrl), timeoutCatalogFutureGetException); } if (catalogResponse.failed()) { - throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", providerUrl, + throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", counterPartyUrl, catalogResponse.getFailureMessages())); } @@ -118,13 +112,13 @@ Dataset getDatasetForAssetId(URL providerUrl, String assetId) throws Interrupted try { modifiedCatalogJson = modifyCatalogJson(catalogResponse.getContent()); } catch (IOException except) { - throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", providerUrl, except)); + throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", counterPartyUrl, except)); } var catalog = transformer.transform(modifiedCatalogJson, Catalog.class); if (catalog.failed()) { - throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", providerUrl, + throw new EdcException(format("Catalog by provider %s couldn't be retrieved: %s", counterPartyUrl, catalog.getFailureMessages())); } @@ -139,9 +133,9 @@ Dataset getDatasetForAssetId(URL providerUrl, String assetId) throws Interrupted return dataset; } - Pair getAcceptablePolicyForAssetId(URL providerUrl, String assetId) + Pair getAcceptablePolicyForAssetId(String counterPartyId, URL providerUrl, String assetId) throws InterruptedException { - var dataset = getDatasetForAssetId(providerUrl, assetId); + var dataset = getDatasetForAssetId(counterPartyId, providerUrl, assetId); Map.Entry acceptablePolicy; if (config.isAcceptAllProviderOffers()) { @@ -172,16 +166,16 @@ private boolean matchesOwnPolicyDefinitions(Policy policy) { private boolean policyDefinitionRulesEquality(Policy first, Policy second) { List firstRules = Stream.of( - first.getPermissions(), - first.getProhibitions(), - first.getObligations()) + first.getPermissions(), + first.getProhibitions(), + first.getObligations()) .flatMap(Collection::stream) .collect(Collectors.toList()); List secondRules = Stream.of( - second.getPermissions(), - second.getProhibitions(), - second.getObligations()) + second.getPermissions(), + second.getProhibitions(), + second.getObligations()) .flatMap(Collection::stream) .collect(Collectors.toList()); 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..bf438097 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java @@ -15,23 +15,11 @@ */ package de.fraunhofer.iosb.client; -import static java.lang.String.format; -import static org.junit.jupiter.api.Assertions.assertEquals; -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.when; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; - +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +import de.fraunhofer.iosb.client.negotiation.NegotiationController; +import de.fraunhofer.iosb.client.policy.PolicyController; +import jakarta.ws.rs.core.Response; import org.eclipse.edc.api.auth.spi.AuthenticationService; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.Dataset; @@ -61,12 +49,22 @@ import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; -import de.fraunhofer.iosb.client.negotiation.NegotiationController; -import de.fraunhofer.iosb.client.policy.PolicyController; -import jakarta.ws.rs.core.Response; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.when; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; public class ClientEndpointTest { @@ -147,7 +145,7 @@ private CatalogService mockCatalogService() throws IOException { var completableFuture = new CompletableFuture>(); completableFuture.complete(StatusResult.success(new ObjectMapper().writeValueAsBytes(mockCatalog))); - when(catalogService.requestCatalog(any(), any(), any())).thenReturn(completableFuture); + when(catalogService.requestCatalog(any(), any(), any(), any())).thenReturn(completableFuture); return catalogService; } @@ -219,7 +217,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 +226,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..e73d0846 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,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + @ExtendWith(DependencyInjectionExtension.class) public class ClientExtensionTest { 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 488f1df6..0d10cd0d 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 @@ -15,19 +15,6 @@ */ package de.fraunhofer.iosb.client.dataTransfer; -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.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Map; -import java.util.UUID; - import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; @@ -39,6 +26,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + public class TransferInitiatorTest { private final TransferProcessManager mockTransferProcessManager = mock(TransferProcessManager.class); diff --git a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java index b99e093f..03b251aa 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java @@ -15,20 +15,6 @@ */ package de.fraunhofer.iosb.client.negotiation; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -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.when; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.stream.Stream; - import org.eclipse.edc.connector.contract.observe.ContractNegotiationObservableImpl; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; @@ -46,24 +32,36 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class NegotiatorTest { - private final ConsumerContractNegotiationManager ccnmMock = mock(ConsumerContractNegotiationManager.class); - private final ContractNegotiationStore cnsMock = mock(ContractNegotiationStore.class); - private final ContractNegotiationObservable contractNegotiationObservable = new ContractNegotiationObservableImpl(); - private final Config configMock = ConfigFactory.empty(); + private final ConsumerContractNegotiationManager ccnmMock = mock(ConsumerContractNegotiationManager.class); + private final ContractNegotiationStore cnsMock = mock(ContractNegotiationStore.class); + private final ContractNegotiationObservable contractNegotiationObservable = new ContractNegotiationObservableImpl(); + private final Config configMock = ConfigFactory.empty(); - private final String assetId = "test-asset-id"; - private final Policy mockPolicy = buildPolicy(); - private final ContractNegotiation negotiation = getContractNegotiation(); + private final String assetId = "test-asset-id"; + private final Policy mockPolicy = buildPolicy(); + private final ContractNegotiation negotiation = getContractNegotiation(); - private Negotiator clientNegotiator; + private Negotiator clientNegotiator; - @BeforeEach - void initializeClientNegotiator() { - defineMockBehaviour(); - clientNegotiator = new Negotiator(ccnmMock, cnsMock, configMock); - } + @BeforeEach + void initializeClientNegotiator() { + defineMockBehaviour(); + clientNegotiator = new Negotiator(ccnmMock, cnsMock, configMock); + } void defineMockBehaviour() { when(cnsMock.queryAgreements(any())).thenReturn(Stream.of()); @@ -71,74 +69,74 @@ void defineMockBehaviour() { .thenReturn(StatusResult.success(negotiation)); } - @Test - void testNegotiate() throws MalformedURLException, ExecutionException, InterruptedException { - // Mocked EDC negotiation manager returns a future which completes to a - // successful negotiation (agreement) - // Input is providerUrl (unimportant), contractOffer. The resulting - // contractAgreement should have the same - // policy as our contractOffer (not the same object reference) and the same - // asset ID - var fakeUrl = new URL("https://example.com/fakeurl"); - var contractOffer = ContractOffer.Builder.newInstance() - .id(UUID.randomUUID().toString()) - .assetId(assetId) - .policy(mockPolicy) - .build(); - - var contractRequest = ContractRequest.Builder.newInstance() - .contractOffer(contractOffer) - .counterPartyAddress(fakeUrl.toString()) - .protocol("dataspace-protocol-http") - .build(); - - var future = Executors.newSingleThreadExecutor() - .submit(() -> clientNegotiator.negotiate(contractRequest)); - - // Let the negotiator add a listener to this negotiation. - // If we don't, the "confirmed" signal will be sent too soon, and the negotiator - // will never see it - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - fail(); - } - contractNegotiationObservable.invokeForEach(listener -> listener.finalized(negotiation)); - - assertNotNull(future); - var contractNegotiation = future.get(); - assertNotNull(contractNegotiation); - assertEquals(mockPolicy, contractNegotiation.getContent().getContractAgreement().getPolicy()); - assertEquals(assetId, contractNegotiation.getContent().getContractAgreement().getAssetId()); + @Test + void testNegotiate() throws MalformedURLException, ExecutionException, InterruptedException { + // Mocked EDC negotiation manager returns a future which completes to a + // successful negotiation (agreement) + // Input is providerUrl (unimportant), contractOffer. The resulting + // contractAgreement should have the same + // policy as our contractOffer (not the same object reference) and the same + // asset ID + var fakeUrl = new URL("https://example.com/fakeurl"); + var contractOffer = ContractOffer.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .assetId(assetId) + .policy(mockPolicy) + .build(); + + var contractRequest = ContractRequest.Builder.newInstance() + .contractOffer(contractOffer) + .counterPartyAddress(fakeUrl.toString()) + .protocol("dataspace-protocol-http") + .build(); + + var future = Executors.newSingleThreadExecutor() + .submit(() -> clientNegotiator.negotiate(contractRequest)); + + // Let the negotiator add a listener to this negotiation. + // If we don't, the "confirmed" signal will be sent too soon, and the negotiator + // will never see it + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + fail(); } + contractNegotiationObservable.invokeForEach(listener -> listener.finalized(negotiation)); - /* - * Policy containing MOCK as permitted action - */ - private Policy buildPolicy() { - return Policy.Builder.newInstance() - .permission(Permission.Builder.newInstance() - .action(Action.Builder.newInstance() - .type("MOCK") - .build()) - .build()) - .build(); - } + assertNotNull(future); + var contractNegotiation = future.get(); + assertNotNull(contractNegotiation); + assertEquals(mockPolicy, contractNegotiation.getContent().getContractAgreement().getPolicy()); + assertEquals(assetId, contractNegotiation.getContent().getContractAgreement().getAssetId()); + } - private ContractNegotiation getContractNegotiation() { - return ContractNegotiation.Builder.newInstance() - .counterPartyId("mock-counter-party-id") - .counterPartyAddress("mock-counter-party-address") - .protocol("mock-protocol") - .id("mocked-negotiation-id") - .contractAgreement(ContractAgreement.Builder.newInstance() - .id(UUID.randomUUID().toString()) - .providerId("provider") - .consumerId("consumer") - .assetId(assetId) - .policy(mockPolicy) - .build()) - .build(); - } + /* + * Policy containing MOCK as permitted action + */ + private Policy buildPolicy() { + return Policy.Builder.newInstance() + .permission(Permission.Builder.newInstance() + .action(Action.Builder.newInstance() + .type("MOCK") + .build()) + .build()) + .build(); + } + + private ContractNegotiation getContractNegotiation() { + return ContractNegotiation.Builder.newInstance() + .counterPartyId("mock-counter-party-id") + .counterPartyAddress("mock-counter-party-address") + .protocol("mock-protocol") + .id("mocked-negotiation-id") + .contractAgreement(ContractAgreement.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .providerId("provider") + .consumerId("consumer") + .assetId(assetId) + .policy(mockPolicy) + .build()) + .build(); + } } diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java index 8a4b4037..33d0f1b3 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java @@ -15,19 +15,7 @@ */ package de.fraunhofer.iosb.client.policy; -import static org.junit.jupiter.api.Assertions.assertEquals; -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.when; - -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - +import de.fraunhofer.iosb.client.testUtils.FileManager; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.DataService; import org.eclipse.edc.catalog.spi.Dataset; @@ -42,7 +30,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.fraunhofer.iosb.client.testUtils.FileManager; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.when; public class PolicyServiceTest { @@ -70,7 +69,7 @@ void getPolicyForAssetIdTest() throws InterruptedException { assert catalogString != null; mockedFuture.complete(StatusResult.success(catalogString.getBytes(StandardCharsets.UTF_8))); - when(mockCatalogService.requestCatalog(any(), any(), any())).thenReturn(mockedFuture); + when(mockCatalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); when(mockTransformer.transform(any(), any())).thenReturn(Result.success(Catalog.Builder.newInstance() .dataset(Dataset.Builder.newInstance() @@ -84,16 +83,16 @@ void getPolicyForAssetIdTest() throws InterruptedException { .build()) .build())); - assertEquals(datasetId, policyService.getDatasetForAssetId(testUrl, "test-asset-id").getId()); + assertEquals(datasetId, policyService.getDatasetForAssetId("provider", testUrl, "test-asset-id").getId()); } @Test void getContractUnreachableProviderTest() throws MalformedURLException, InterruptedException { var mockedFuture = new CompletableFuture>(); - when(mockCatalogService.requestCatalog(any(), any(), any())).thenReturn(mockedFuture); + when(mockCatalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); try { - policyService.getDatasetForAssetId(new URL("http://fakeUrl:4321/not/working"), "test-asset-id"); + policyService.getDatasetForAssetId("provider", new URL("http://fakeUrl:4321/not/working"), "test-asset-id"); fail("This should not complete without throwing an exception"); } catch (EdcException expected) { } diff --git a/gradle.properties b/gradle.properties index e76f90c1..7b9c0166 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ javaVersion=17 group=org.eclipse.edc -edcVersion=0.4.1 +edcVersion=0.5.1 faaastVersion=0.5.0 rsApi=3.1.0 okHttpVersion=4.10.0 From 9e00bcd05235005869aa2dc4ea15d58e5e4ba93b Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:00:56 +0100 Subject: [PATCH 02/24] Code cleanup --- .../dataTransfer/DataTransferController.java | 29 +++++++++---------- .../iosb/client/negotiation/Negotiator.java | 7 ++--- .../iosb/client/policy/PolicyController.java | 8 ++--- .../client/policy/PolicyDefinitionStore.java | 1 - .../dataTransfer/TransferInitiatorTest.java | 5 ++-- 5 files changed, 21 insertions(+), 29 deletions(-) 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 4a9dbc51..ec3a7377 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 @@ -42,7 +42,6 @@ public class DataTransferController { private final Config config; - private final DataTransferEndpoint dataTransferEndpoint; private final DataTransferObservable dataTransferObservable; private final TransferInitiator transferInitiator; @@ -51,14 +50,14 @@ public class DataTransferController { /** * Class constructor * - * @param monitor Logging. - * @param config Read config value transfer timeout and - * own URI - * @param webService Register data transfer endpoint. - * @param dataEndpointAuthRequestFilter 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 authenticationService Creating and passing through custom api + * keys for each data transfer. + * @param transferProcessManager Initiating a transfer process as a + * consumer. */ public DataTransferController(Monitor monitor, Config config, WebService webService, AuthenticationService authenticationService, TransferProcessManager transferProcessManager) { @@ -68,7 +67,7 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ authenticationService); this.dataTransferObservable = new DataTransferObservable(monitor); - this.dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); + var dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); webService.registerResource(dataTransferEndpoint); } @@ -76,11 +75,11 @@ 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) + * @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 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 f1b57f70..588c465a 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,13 +37,11 @@ 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 */ public Negotiator(ConsumerContractNegotiationManager consumerNegotiationManager, - ContractNegotiationStore contractNegotiationStore, Config config) { + ContractNegotiationStore contractNegotiationStore) { this.consumerNegotiationManager = consumerNegotiationManager; this.contractNegotiationStore = contractNegotiationStore; } @@ -56,8 +54,7 @@ public Negotiator(ConsumerContractNegotiationManager consumerNegotiationManager, * aborted by throwing an exception. This exception can be inspected using the * getCause() method. */ - StatusResult negotiate(ContractRequest contractRequest) - throws InterruptedException, ExecutionException { + StatusResult negotiate(ContractRequest contractRequest) { var previousAgreements = contractNegotiationStore.queryAgreements(QuerySpec.max()); var relevantAgreements = previousAgreements .filter(agreement -> agreement.getAssetId().equals(contractRequest.getContractOffer().getAssetId())) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java index 0a2cd034..8d7c7340 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyController.java @@ -35,17 +35,15 @@ public class PolicyController { - private final PolicyServiceConfig config; - private final PolicyDefinitionStore policyDefinitionStore; private final PolicyService policyService; public PolicyController(Monitor monitor, CatalogService catalogService, TypeTransformerRegistry typeTransformerRegistry, Config systemConfig) { - this.config = new PolicyServiceConfig(systemConfig); + var config = new PolicyServiceConfig(systemConfig); - this.policyDefinitionStore = new PolicyDefinitionStore(monitor, this.config.getAcceptedPolicyDefinitionsPath()); - this.policyService = new PolicyService(catalogService, typeTransformerRegistry, this.config, + this.policyDefinitionStore = new PolicyDefinitionStore(monitor, config.getAcceptedPolicyDefinitionsPath()); + this.policyService = new PolicyService(catalogService, typeTransformerRegistry, config, this.policyDefinitionStore); } diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java index 62dc25da..762ccaa5 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java @@ -77,7 +77,6 @@ Optional removePolicyDefinition(String policyDefinitionId) { /** * Update a policyDefinition * - * @param policyDefinitionId PolicyDefinition ID (non null) * @param policyDefinition The updated policyDefinition * @return Optional containing updated policy definition or null */ 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 0d10cd0d..496b61c8 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 @@ -39,15 +39,14 @@ public class TransferInitiatorTest { private final TransferProcessManager mockTransferProcessManager = mock(TransferProcessManager.class); - private Config configMock; private TransferInitiator transferInitiator; private StatusResult mockStatusResult; @BeforeEach @SuppressWarnings("unchecked") - void initializeContractOfferService() throws URISyntaxException { - configMock = ConfigFactory.fromMap(Map.of("edc.dsp.callback.address", "http://localhost:4321/dsp", + 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); From 4bea0148ac05ca7aa61eaeb50025f657f999634f Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:01:54 +0100 Subject: [PATCH 03/24] Code cleanup --- .../iosb/client/negotiation/NegotiationController.java | 2 +- .../java/de/fraunhofer/iosb/client/negotiation/Negotiator.java | 3 --- .../iosb/client/dataTransfer/TransferInitiatorTest.java | 2 -- .../de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java index 0cfcd88d..f5286247 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java @@ -51,7 +51,7 @@ public NegotiationController(ConsumerContractNegotiationManager consumerNegotiat ContractNegotiationObservable observable, ContractNegotiationStore contractNegotiationStore, Config config) { this.config = config; - this.negotiator = new Negotiator(consumerNegotiationManager, contractNegotiationStore, config); + this.negotiator = new Negotiator(consumerNegotiationManager, contractNegotiationStore); this.listener = new ClientContractNegotiationListener(); observable.registerListener(listener); } 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 588c465a..c95ae59f 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 @@ -21,9 +21,6 @@ import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.response.StatusResult; -import org.eclipse.edc.spi.system.configuration.Config; - -import java.util.concurrent.ExecutionException; /** * Send contractrequest, negotiation status watch 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 496b61c8..01c86d6a 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 @@ -21,13 +21,11 @@ import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.response.StatusResult; -import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.system.configuration.ConfigFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.net.URL; import java.util.Map; import java.util.UUID; diff --git a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java index 03b251aa..937b3592 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java @@ -60,7 +60,7 @@ public class NegotiatorTest { @BeforeEach void initializeClientNegotiator() { defineMockBehaviour(); - clientNegotiator = new Negotiator(ccnmMock, cnsMock, configMock); + clientNegotiator = new Negotiator(ccnmMock, cnsMock); } void defineMockBehaviour() { From e41dc4e1cac3159ac4c854401b2656d73d11d6ed Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:07:45 +0100 Subject: [PATCH 04/24] Update docs --- README.md | 132 +++++++++--------- changelog.md | 5 +- .../iosb/client/ClientEndpoint.java | 3 +- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 089a10be..5c812d11 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ model via the EDC. ## Version compatibility -| Specification | Version | -|:-----------------------|-------------------------| -| Eclipse Dataspace Connector | v0.4.1 | +| Specification | Version | +|:-----------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| Eclipse Dataspace Connector | v0.4.1 | | AAS - Details of the Asset Administration Shell - Part 1
The exchange of information between partners in the value chain of Industrie 4.0 | Version 3.0RC01
(based on [admin-shell-io/java-model](https://github.com/admin-shell-io/java-model)) | ## Repo Structure The repository contains several material: + - `client`: Source code for the client extension (automated contract negotiation) - `config`: Checkstyle files for code formatting - `edc-extension4aas`: Source code for the AAS extension @@ -36,7 +37,8 @@ Assets. Furthermore, this extension can also start AAS by reading an AAS model. applied for all elements. For critical elements, additional contracts can be placed. External changes to the model of an AAS are automatically synchronized by the extension. -Additionally, a client extension providing API calls for aggregations of processes such as contract negotiation and data transfer +Additionally, a client extension providing API calls for aggregations of processes such as contract negotiation and data +transfer is available. ### Use Cases @@ -49,90 +51,88 @@ Provide digital twin (AAS) data to business partners in Data Spaces like Catena- #### **Provider Interfaces** -| HTTP Method | Interface (edc:1234/api/...) ((a) = only for authenticated users) | Parameters ((r) = required) | Description | -|:-|:-|:-|:-| -| GET | config (a) | - | Get current extension configuration values. | -| PUT | config (a) | Body: Updated config values (JSON) (r) | Update config values. | -| POST | client (a) | Query Parameter "url" (r) | Register a standalone AAS service (e.g., FA³ST) to this extension. | -| DELETE | client (a) | Query Parameter "url" (r) | Unregister an AAS service (e.g., FA³ST) from this extension, possibly shutting down the service if it has been started internally. | -| POST | environment (a) | Query Parameter "environment": Path to new AAS environment (r), Query Parameter "port": Port of service to be created , Query Parameter "config": Path of AAS service configuration file | Create a new AAS service. Either (http) "port" or "config" must be given to ensure communication with the AAS service via an HTTP endpoint on the service's side. This command returns the URL of the newly created AAS service on success, which can be used to remove the service using the interface "DELETE /client" | -| POST | aas (a) | Query Parameter "requestUrl": URL of AAS service to be updated (r), request body: AAS element (r) | Forward POST request to provided host in requestUrl. If requestUrl is an AAS service that is registered at this EDC, synchronize assets and self description as well. | -| DELETE | aas (a) | Query Parameter requestUrl: URL of AAS service to be updated (r) | Forward DELETE request to provided host in requestUrl. If requestUrl is an AAS service that is registered at this EDC, synchronize assets and self description as well. | -| PUT | aas (a) | Query Parameter "requestUrl": URL of AAS service to be updated (r), request body: AAS element (r) | Forward PUT request to provided host in requestUrl. | -| GET | selfDescription | - | Return self description of extension. | +| HTTP Method | Interface (edc:1234/api/...) ((a) = only for authenticated users) | Parameters ((r) = required) | Description | +|:------------|:------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| GET | config (a) | - | Get current extension configuration values. | +| PUT | config (a) | Body: Updated config values (JSON) (r) | Update config values. | +| POST | client (a) | Query Parameter "url" (r) | Register a standalone AAS service (e.g., FA³ST) to this extension. | +| DELETE | client (a) | Query Parameter "url" (r) | Unregister an AAS service (e.g., FA³ST) from this extension, possibly shutting down the service if it has been started internally. | +| POST | environment (a) | Query Parameter "environment": Path to new AAS environment (r), Query Parameter "port": Port of service to be created , Query Parameter "config": Path of AAS service configuration file | Create a new AAS service. Either (http) "port" or "config" must be given to ensure communication with the AAS service via an HTTP endpoint on the service's side. This command returns the URL of the newly created AAS service on success, which can be used to remove the service using the interface "DELETE /client" | +| POST | aas (a) | Query Parameter "requestUrl": URL of AAS service to be updated (r), request body: AAS element (r) | Forward POST request to provided host in requestUrl. If requestUrl is an AAS service that is registered at this EDC, synchronize assets and self description as well. | +| DELETE | aas (a) | Query Parameter requestUrl: URL of AAS service to be updated (r) | Forward DELETE request to provided host in requestUrl. If requestUrl is an AAS service that is registered at this EDC, synchronize assets and self description as well. | +| PUT | aas (a) | Query Parameter "requestUrl": URL of AAS service to be updated (r), request body: AAS element (r) | Forward PUT request to provided host in requestUrl. | +| GET | selfDescription | - | Return self description of extension. | #### **Client Interfaces** -| HTTP Method | Interface (edc:1234/api/automated/...) ((a) = only for authenticated users) | Parameters ((r) = required) | Description | -|:-|:-|:-|:-| -| POST | negotiate (a) | Query Parameter "providerUrl": URL (r),Query Parameter "providerId": String (r), Query Parameter "assetId": String (r), Query Parameter "dataDestinationUrl": URL | Perform an automated contract negotiation with a provider (given provider URL and ID) and get the data stored for the specified asset. Optionally, a data destination URL can be specified where the data is sent to instead of the extension's log. | -| GET | dataset (a) | Query Parameter "providerUrl": URL (r), Query Parameter "assetId": String (r) | Get dataset from the specified provider's catalog that contains the specified asset's policies. | -| POST | negotiateContract (a) | request body: org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest (r) | Using a contractRequest (JSON in http request body), negotiate a contract. Returns the corresponding agreementId on success. | -| GET | transfer (a) | Query Parameter "providerUrl": URL (r), Query Parameter "agreementId": String (r), Query Parameter "assetId": String (r), Query Parameter "dataDestinationUrl" | Submits a data transfer request to the providerUrl. On success, returns the data behind the specified asset. Optionally, a data destination URL can be specified where the data is sent to instead of the extension's log. | - -| POST | acceptedPolicies (a) | request body: List of PolicyDefinitions (JSON) (r) | Adds the given PolicyDefinitions to the accepted PolicyDefinitions list (Explanation: On fully automated negotiation, the provider's PolicyDefinition is matched against the consumer's accepted PolicyDefinitions list. If any PolicyDefinition fits the provider's, the negotiation continues.) Returns "OK"-Response if requestBody is valid. | -| GET | acceptedPolicies (a) | - | Returns the client extension's accepted policy definitions for fully automated negotiation. | -| DELETE | acceptedPolicies (a) | request body: PolicyDefinition: PolicyDefinition (JSON) (r) | Updates the client extension's accepted policy definition with the same policyDefinitionId as the request. | -| PUT | acceptedPolicies (a) | request body: PolicyDefinitionId: String (JSON) (r) | Deletes a client extension's accepted policy definition with the same policyDefinitionId as the request. | - +| HTTP Method | Interface (edc:1234/api/automated/...) ((a) = only for authenticated users) | Parameters ((r) = required) | Description | +|:------------|:----------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| POST | negotiate (a) | Query Parameter "providerUrl": URL (r), Query Parameter "providerId": String (r), Query Parameter "assetId": String (r), Query Parameter "dataDestinationUrl": URL | Perform an automated contract negotiation with a provider (given provider URL and ID) and get the data stored for the specified asset. Optionally, a data destination URL can be specified where the data is sent to instead of the extension's log. | +| GET | dataset (a) | Query Parameter "providerUrl": URL (r), Query Parameter "assetId": String (r), Query Parameter "providerId": String (r) | Get dataset from the specified provider's catalog that contains the specified asset's policies. | +| POST | negotiateContract (a) | request body: org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest (r) | Using a contractRequest (JSON in http request body), negotiate a contract. Returns the corresponding agreementId on success. | +| GET | transfer (a) | Query Parameter "providerUrl": URL (r), Query Parameter "agreementId": String (r), Query Parameter "assetId": String (r), Query Parameter "dataDestinationUrl" | Submits a data transfer request to the providerUrl. On success, returns the data behind the specified asset. Optionally, a data destination URL can be specified where the data is sent to instead of the extension's log. | +| POST | acceptedPolicies (a) | request body: List of PolicyDefinitions (JSON) (r) | Adds the given PolicyDefinitions to the accepted PolicyDefinitions list (Explanation: On fully automated negotiation, the provider's PolicyDefinition is matched against the consumer's accepted PolicyDefinitions list. If any PolicyDefinition fits the provider's, the negotiation continues.) Returns "OK"-Response if requestBody is valid. | +| GET | acceptedPolicies (a) | - | Returns the client extension's accepted policy definitions for fully automated negotiation. | +| DELETE | acceptedPolicies (a) | request body: PolicyDefinition: PolicyDefinition (JSON) (r) | Updates the client extension's accepted policy definition with the same policyDefinitionId as the request. | +| PUT | acceptedPolicies (a) | request body: PolicyDefinitionId: String (JSON) (r) | Deletes a client extension's accepted policy definition with the same policyDefinitionId as the request. | ### Dependencies #### EDC-Extension-for-AAS -| Name | Description | -|:-|:-| -| de.fraunhofer.iosb.ilt.faaast.service:starter | [FA³ST Service](https://github.com/FraunhoferIOSB/FAAAST-Service) to start AAS services internally. | -| io.admin-shell.aas:dataformat-json | [admin-shell-io java serializer](https://github.com/admin-shell-io/java-serializer) (de-)serialize AAS models | -| io.admin-shell.aas:model | [admin-shell-io java model](https://github.com/admin-shell-io/java-model) (de-)serialize AAS models | -| com.squareup.okhttp3:okhttp | Send HTTP requests to AAS services | -| jakarta.ws.rs:jakarta.ws.rs-api | provides HTTP endpoints of extension | -| org.eclipse.edc:contract-core | Client: Observe contract negotiation state | -| org.eclipse.edc:management-api | EDC asset/contract management | -| org.eclipse.edc:runtime-metamodel | EDC metamodel | -| org.eclipse.edc:dsp-catalog-http-dispatcher | EDC constants | +| Name | Description | +|:----------------------------------------------|:--------------------------------------------------------------------------------------------------------------| +| de.fraunhofer.iosb.ilt.faaast.service:starter | [FA³ST Service](https://github.com/FraunhoferIOSB/FAAAST-Service) to start AAS services internally. | +| io.admin-shell.aas:dataformat-json | [admin-shell-io java serializer](https://github.com/admin-shell-io/java-serializer) (de-)serialize AAS models | +| io.admin-shell.aas:model | [admin-shell-io java model](https://github.com/admin-shell-io/java-model) (de-)serialize AAS models | +| com.squareup.okhttp3:okhttp | Send HTTP requests to AAS services | +| jakarta.ws.rs:jakarta.ws.rs-api | provides HTTP endpoints of extension | +| org.eclipse.edc:contract-core | Client: Observe contract negotiation state | +| org.eclipse.edc:management-api | EDC asset/contract management | +| org.eclipse.edc:runtime-metamodel | EDC metamodel | +| org.eclipse.edc:dsp-catalog-http-dispatcher | EDC constants | #### Client Extension -| Name | Description | -|:-|:-| -| org.eclipse.edc:contract-core | Client: Observe contract negotiation state | -| org.eclipse.edc:dsp-catalog-http-dispatcher | EDC constants | -| org.eclipse.edc:management-api | EDC asset/contract management | -| org.eclipse.edc:runtime-metamodel | EDC metamodel | -| org.eclipse.edc:data-plane-http-spi | HttpDataAddress | -| jakarta.ws.rs:jakarta.ws.rs-api | provides HTTP endpoints of extension | +| Name | Description | +|:--------------------------------------------|:-------------------------------------------| +| org.eclipse.edc:contract-core | Client: Observe contract negotiation state | +| org.eclipse.edc:dsp-catalog-http-dispatcher | EDC constants | +| org.eclipse.edc:management-api | EDC asset/contract management | +| org.eclipse.edc:runtime-metamodel | EDC metamodel | +| org.eclipse.edc:data-plane-http-spi | HttpDataAddress | +| jakarta.ws.rs:jakarta.ws.rs-api | provides HTTP endpoints of extension | ### Configurations #### **EDC-Extension-for-AAS Configurations** -| Key| Value Type| Description| -|:-|:-|:-| -| edc.aas.remoteAasLocation| URL| A URL of an AAS service (such as FA³ST) that is already running and is conformant with official AAS API specification| -| edc.aas.localAASModelPath| path| A path to a serialized AAS environment compatible to specification version 3.0RC01 (see: https://github.com/FraunhoferIOSB/FAAAST-Service/blob/main/README.md) | -| edc.aas.localAASServicePort| Open port from 1 to 65535 | Port to locally created AAS service. Required, if localAASModelPath is defined and localAASServiceConfigPath is not defined.| -| edc.aas.localAASServiceConfigPath | path| Path to AAS config for locally started AAS service. Required, if localAASServicePort is not defined, but localAASModelPath is defined.| -| edc.aas.syncPeriod | whole number in seconds | Time period in which AAS services should be polled for structural changes (added/deleted elements etc.). Default value is 5 (seconds). Note: This configuration value is only read on startup, the synchronization period cannot be changed at runtime. | -| edc.aas.exposeSelfDescription| true/false| Whether the Self Description should be exposed on {edc}/api/selfDescription. When set to False, the selfDescription is still available for authenticated requests.| -| edc.aas.defaultAccessPolicyPath| path| Path to an access policy file (JSON). This policy will be used as the default access policy for all assets created after the configuration value has been set.| -| edc.aas.defaultContractPolicyPath | path| Path to a contract policy file (JSON). This policy will be used as the default contract policy for all assets created after the configuration value has been set.| +| Key | Value Type | Description | +|:----------------------------------|:--------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| edc.aas.remoteAasLocation | URL | A URL of an AAS service (such as FA³ST) that is already running and is conformant with official AAS API specification | +| edc.aas.localAASModelPath | path | A path to a serialized AAS environment compatible to specification version 3.0RC01 (see: https://github.com/FraunhoferIOSB/FAAAST-Service/blob/main/README.md) | +| edc.aas.localAASServicePort | Open port from 1 to 65535 | Port to locally created AAS service. Required, if localAASModelPath is defined and localAASServiceConfigPath is not defined. | +| edc.aas.localAASServiceConfigPath | path | Path to AAS config for locally started AAS service. Required, if localAASServicePort is not defined, but localAASModelPath is defined. | +| edc.aas.syncPeriod | whole number in seconds | Time period in which AAS services should be polled for structural changes (added/deleted elements etc.). Default value is 5 (seconds). Note: This configuration value is only read on startup, the synchronization period cannot be changed at runtime. | +| edc.aas.exposeSelfDescription | true/false | Whether the Self Description should be exposed on {edc}/api/selfDescription. When set to False, the selfDescription is still available for authenticated requests. | +| edc.aas.defaultAccessPolicyPath | path | Path to an access policy file (JSON). This policy will be used as the default access policy for all assets created after the configuration value has been set. | +| edc.aas.defaultContractPolicyPath | path | Path to a contract policy file (JSON). This policy will be used as the default contract policy for all assets created after the configuration value has been set. | #### **Client Extension Configurations** -| Key | Value Type | Description | -|:-|:-|:-| -| edc.client.waitForAgreementTimeout | whole number in seconds | How long should the extension wait for an agreement when automatically negotiating a contract? Default value is 10(s). | -| edc.client.waitForTransferTimeout | whole number in seconds | How long should the extension wait for a data transfer when automatically negotiating a contract? Default value is 10(s). | -| edc.client.acceptAllProviderOffers | boolean | If true, the client accepts any contractOffer offered by a provider connector on automated contract negotiation (e.g., trusted provider). Default value: false | -| edc.client.acceptedPolicyDefinitionsPath | path | Path pointing to a JSON-file containing acceptable PolicyDefinitions for automated contract negotiation in a list (only policies must match in a provider's PolicyDefinition) | +| Key | Value Type | Description | +|:-----------------------------------------|:------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| edc.client.waitForAgreementTimeout | whole number in seconds | How long should the extension wait for an agreement when automatically negotiating a contract? Default value is 10(s). | +| edc.client.waitForTransferTimeout | whole number in seconds | How long should the extension wait for a data transfer when automatically negotiating a contract? Default value is 10(s). | +| edc.client.acceptAllProviderOffers | boolean | If true, the client accepts any contractOffer offered by a provider connector on automated contract negotiation (e.g., trusted provider). Default value: false | +| edc.client.acceptedPolicyDefinitionsPath | path | Path pointing to a JSON-file containing acceptable PolicyDefinitions for automated contract negotiation in a list (only policies must match in a provider's PolicyDefinition) | ## Terminology -| Term | Description | -|:-|:-| -| AAS | Asset Administration Shell (see [AAS reading guide](https://www.plattform-i40.de/IP/Redaktion/DE/Downloads/Publikation/Asset_Administration_Shell_Reading_Guide.html) or [AAS specification part 1](https://www.plattform-i40.de/IP/Redaktion/DE/Downloads/Publikation/Details_of_the_Asset_Administration_Shell_Part1_V3.html)) | -| FA³ST Service | Open Source java implementation of the AAS part 2 [see on GitHub](https://github.com/FraunhoferIOSB/FAAAST-Service) | +| Term | Description | +|:--------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| AAS | Asset Administration Shell (see [AAS reading guide](https://www.plattform-i40.de/IP/Redaktion/DE/Downloads/Publikation/Asset_Administration_Shell_Reading_Guide.html) or [AAS specification part 1](https://www.plattform-i40.de/IP/Redaktion/DE/Downloads/Publikation/Details_of_the_Asset_Administration_Shell_Part1_V3.html)) | +| FA³ST Service | Open Source java implementation of the AAS part 2 [see on GitHub](https://github.com/FraunhoferIOSB/FAAAST-Service) | ## Roadmap diff --git a/changelog.md b/changelog.md index a04229ed..0b06e90a 100644 --- a/changelog.md +++ b/changelog.md @@ -2,10 +2,13 @@ ## Current development version -Compatibility: **Eclipse Dataspace Connector v0.4.1** +Compatibility: **Eclipse Dataspace Connector v0.5.1** **New Features** +- counterPartyId now needed when using client extension + + **Bugfixes** ## V1.0.0-alpha5 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 f909cef9..fa9acfe4 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java @@ -200,7 +200,8 @@ public Response negotiateContract(ContractRequest contractRequest) { @GET @Path(TRANSFER_PATH) public Response getData(@QueryParam("providerUrl") URL providerUrl, - @QueryParam("agreementId") String agreementId, @QueryParam("assetId") String assetId, + @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"); From 419504df9878115c2e7f8ff654891513ac32abf7 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:25:54 +0100 Subject: [PATCH 05/24] Code cleanup --- .../iosb/client/policy/PolicyService.java | 35 ++----------------- .../dataTransfer/TransferInitiatorTest.java | 1 - .../client/negotiation/NegotiatorTest.java | 1 - 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java index 4c119cd7..22665f8e 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java @@ -15,33 +15,11 @@ */ package de.fraunhofer.iosb.client.policy; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; import de.fraunhofer.iosb.client.util.Pair; import jakarta.json.Json; -import jakarta.json.JsonObject; -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.query.Criterion.criterion; - -import java.io.ByteArrayInputStream; -import java.net.URL; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.eclipse.edc.catalog.spi.Catalog; -import org.eclipse.edc.catalog.spi.DataService; import org.eclipse.edc.catalog.spi.Dataset; -import org.eclipse.edc.catalog.spi.Distribution; import org.eclipse.edc.connector.spi.catalog.CatalogService; import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.spi.JsonLd; @@ -55,7 +33,6 @@ import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.net.URL; import java.util.Collection; import java.util.List; @@ -68,16 +45,8 @@ import java.util.stream.Stream; import static java.lang.String.format; -import static org.eclipse.edc.jsonld.spi.Namespaces.*; -import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCAT_ACCESS_SERVICE_ATTRIBUTE; -import static org.eclipse.edc.jsonld.spi.PropertyAndTypeNames.DCT_FORMAT_ATTRIBUTE; -import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_PREFIX; -import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; import static org.eclipse.edc.spi.query.Criterion.criterion; -import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; -import de.fraunhofer.iosb.client.util.Pair; -import jakarta.json.Json; /** * Finds out policy for a given asset id and provider EDC url @@ -87,7 +56,7 @@ class PolicyService { private static final String CATALOG_RETRIEVAL_FAILURE_MSG = "Catalog by provider %s couldn't be retrieved: %s"; private final CatalogService catalogService; private final TypeTransformerRegistry transformer; - + private final PolicyServiceConfig config; private final PolicyDefinitionStore policyDefinitionStore; @@ -100,7 +69,7 @@ class PolicyService { * @param transformer Transform json-ld byte-array catalog to catalog class */ public PolicyService(CatalogService catalogService, TypeTransformerRegistry transformer, - PolicyServiceConfig config, PolicyDefinitionStore policyDefinitionStore, Monitor monitor) { + PolicyServiceConfig config, PolicyDefinitionStore policyDefinitionStore, Monitor monitor) { this.catalogService = catalogService; this.transformer = transformer; 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 01c86d6a..b2776664 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 @@ -31,7 +31,6 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; public class TransferInitiatorTest { diff --git a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java index 937b3592..d6107c49 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java @@ -49,7 +49,6 @@ public class NegotiatorTest { private final ConsumerContractNegotiationManager ccnmMock = mock(ConsumerContractNegotiationManager.class); private final ContractNegotiationStore cnsMock = mock(ContractNegotiationStore.class); private final ContractNegotiationObservable contractNegotiationObservable = new ContractNegotiationObservableImpl(); - private final Config configMock = ConfigFactory.empty(); private final String assetId = "test-asset-id"; private final Policy mockPolicy = buildPolicy(); From fbf0bdb310ddbb17fdc59b2ff50b34719f99dc6d Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sun, 25 Feb 2024 17:20:50 +0100 Subject: [PATCH 06/24] Code cleanup --- .../de/fraunhofer/iosb/client/policy/PolicyService.java | 6 ++++-- .../java/de/fraunhofer/iosb/client/ClientExtensionTest.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java index 22665f8e..8491e3a8 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java @@ -31,6 +31,7 @@ import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.net.URL; @@ -80,13 +81,14 @@ public PolicyService(CatalogService catalogService, TypeTransformerRegistry tran } - Dataset getDatasetForAssetId(String counterPartyId, URL counterPartyUrl, String assetId) throws InterruptedException { + Dataset getDatasetForAssetId(@NotNull String counterPartyId, @NotNull URL counterPartyUrl, @NotNull String assetId) throws InterruptedException { + var catalogFuture = catalogService.requestCatalog( counterPartyId, // why do we even need a provider id when we have the url... counterPartyUrl.toString(), DATASPACE_PROTOCOL_HTTP, QuerySpec.Builder.newInstance() - .filter(List.of(criterion(Asset.PROPERTY_ID, "=", assetId))) + .filter(criterion(Asset.PROPERTY_ID, "=", assetId)) .build()); StatusResult catalogResponse; 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 e73d0846..11b3922d 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientExtensionTest.java @@ -30,8 +30,8 @@ void setup(ServiceExtensionContext context, ObjectFactory factory) { context.registerService(CatalogService.class, mock(CatalogService.class)); context.registerService(ConsumerContractNegotiationManager.class, mock(ConsumerContractNegotiationManager.class)); - context.registerService(ContractNegotiationStore.class, mock(ContractNegotiationStore.class)); context.registerService(ContractNegotiationObservable.class, mock(ContractNegotiationObservable.class)); + context.registerService(ContractNegotiationStore.class, mock(ContractNegotiationStore.class)); context.registerService(TransferProcessManager.class, mock(TransferProcessManager.class)); context.registerService(WebService.class, mock(WebService.class)); context.registerService(Monitor.class, mock(Monitor.class)); From ffe9908f4ae0880508b2fb2f17259e5fa955b660 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sun, 25 Feb 2024 17:21:25 +0100 Subject: [PATCH 07/24] Write new tests for PolicyService --- .../iosb/client/policy/PolicyServiceTest.java | 163 ++++++++++++++---- 1 file changed, 129 insertions(+), 34 deletions(-) diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java index 6aa15308..58ddec90 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java @@ -15,6 +15,7 @@ */ package de.fraunhofer.iosb.client.policy; +import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; import de.fraunhofer.iosb.client.testUtils.FileManager; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.DataService; @@ -24,86 +25,180 @@ import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +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.query.Criterion.criterion; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +/** + * We assume here that catalogService does not return null objects as well as null catalogs inside their return value. + * Also, we assume that catalogs are valid JSON and expandable by the connector's JSON LD expander TitaniumJsonLd.class. + * Finally, we assume that catalogs can be transformed with the TypeTransformerRegistry. + */ public class PolicyServiceTest { private final int providerPort = 54321; - private final CatalogService mockCatalogService = mock(CatalogService.class); - private final TypeTransformerRegistry mockTransformer = mock(TypeTransformerRegistry.class); - + private CatalogService catalogService; + private TypeTransformerRegistry typeTransformerRegistry; + private PolicyServiceConfig config; private PolicyService policyService; - private final URL testUrl = new URL("http://localhost:" + providerPort); + private final URL testUrl; public PolicyServiceTest() throws MalformedURLException { + testUrl = new URL("http://localhost:" + providerPort); } + @BeforeEach - void initializeContractOfferService() { - policyService = new PolicyService(mockCatalogService, mockTransformer, mockConfig(), - mock(PolicyDefinitionStore.class), mock(Monitor.class)); + void initializePolicyService() { + catalogService = mock(CatalogService.class); + typeTransformerRegistry = mock(TypeTransformerRegistry.class); + var policyDefinitionStore = mock(PolicyDefinitionStore.class); + config = mock(PolicyServiceConfig.class); + + policyService = new PolicyService(catalogService, typeTransformerRegistry, config, policyDefinitionStore, mock(Monitor.class)); + } + + @Test + void getDatasetCatalogResponseFailureTest() throws InterruptedException { + var querySpec = QuerySpec.Builder.newInstance().filter(List.of(criterion(Asset.PROPERTY_ID, "=", "test-asset-id"))).build(); + var future = new CompletableFuture>(); + future.complete(StatusResult.failure(ResponseStatus.FATAL_ERROR, "This is a test")); + + when(catalogService.requestCatalog("test-counter-party-id", testUrl.toString(), DATASPACE_PROTOCOL_HTTP, querySpec)).thenReturn(future); + + try { + policyService.getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id"); + fail(); // Should throw exception + } catch (EdcException expected) { + assertEquals(format("Catalog by provider %s couldn't be retrieved: %s", testUrl, "[This is a test]"), expected.getMessage()); + } + } + + @Test + void getDatasetCatalogFutureTimeoutTest() throws InterruptedException { + var querySpec = QuerySpec.Builder.newInstance().filter(criterion(Asset.PROPERTY_ID, "=", "test-asset-id")).build(); + var future = new CompletableFuture>(); + + when(catalogService.requestCatalog("test-counter-party-id", testUrl.toString(), DATASPACE_PROTOCOL_HTTP, querySpec)).thenReturn(future); + + try { + policyService.getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id"); + fail(); // Should throw exception + } catch (EdcException expected) { + assertEquals(format("Timeout while waiting for catalog by provider %s.", testUrl), expected.getMessage()); + } + } + + @Test + void getDatasetNoDatasetsTest() throws InterruptedException { + var querySpec = QuerySpec.Builder.newInstance().filter(criterion(Asset.PROPERTY_ID, "=", "test-asset-id")).build(); + var future = new CompletableFuture>(); + var catalogString = FileManager.loadResource("catalog.json"); + assert catalogString != null; + future.complete(StatusResult.success(catalogString.getBytes(StandardCharsets.UTF_8))); + + when(catalogService.requestCatalog("test-counter-party-id", testUrl.toString(), DATASPACE_PROTOCOL_HTTP, querySpec)).thenReturn(future); + when(typeTransformerRegistry.transform(any(), any())) + .thenReturn(Result.success(Catalog.Builder.newInstance().build())); + + try { + policyService.getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id"); + fail(); // Should throw exception + } catch (AmbiguousOrNullException expected) { + assertEquals(format("Multiple or no policyDefinitions were found for assetId %s!", "test-asset-id"), expected.getMessage()); + } } @Test - void getPolicyForAssetIdTest() throws InterruptedException { + void getDatasetTest() throws InterruptedException { var mockedFuture = new CompletableFuture>(); var datasetId = "ef4d028f-70d7-404a-b22e-c5b0ffa3aa0b"; var catalogString = FileManager.loadResource("catalog.json"); assert catalogString != null; mockedFuture.complete(StatusResult.success(catalogString.getBytes(StandardCharsets.UTF_8))); - when(mockCatalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); + when(catalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); - when(mockTransformer.transform(any(), any())).thenReturn(Result.success(Catalog.Builder.newInstance() - .dataset(Dataset.Builder.newInstance() - .id(datasetId) - .offer(UUID.randomUUID().toString(), - Policy.Builder.newInstance().build()) - .distribution(Distribution.Builder.newInstance() - .dataService(DataService.Builder.newInstance().build()) - .format("") - .build()) - .build()) - .build())); + when(typeTransformerRegistry.transform(any(), any())).thenReturn(Result.success(Catalog.Builder.newInstance().dataset(Dataset.Builder.newInstance().id(datasetId).offer(UUID.randomUUID().toString(), Policy.Builder.newInstance().build()).distribution(Distribution.Builder.newInstance().dataService(DataService.Builder.newInstance().build()).format("").build()).build()).build())); assertEquals(datasetId, policyService.getDatasetForAssetId("provider", testUrl, "test-asset-id").getId()); } @Test - void getContractUnreachableProviderTest() throws MalformedURLException, InterruptedException { - var mockedFuture = new CompletableFuture>(); - when(mockCatalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); + void getAcceptablePolicyForAssetIdTest() throws InterruptedException { + var shouldPolicy = Policy.Builder.newInstance().build(); + var dataset = Dataset.Builder.newInstance() + .offer("test-offer-id", shouldPolicy).build(); + // mock getDatasetMethod + var policyServiceSpy = spy(policyService); + Mockito.doReturn(dataset) + .when(policyServiceSpy) + .getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id"); + when(config.isAcceptAllProviderOffers()).thenReturn(true); + + var resultPolicy = policyServiceSpy.getAcceptablePolicyForAssetId("test-counter-party-id", testUrl, "test-asset-id"); + assertEquals(shouldPolicy, resultPolicy.getSecond()); + assertEquals("test-offer-id", resultPolicy.getFirst()); + } + + @Test + void getAcceptablePolicyForAssetIdEmptyPolicyListTest() throws InterruptedException { + // mock getDatasetMethod + var policyServiceSpy = spy(policyService); + Mockito.doReturn(Dataset.Builder.newInstance().build()).when(policyServiceSpy).getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id"); try { - policyService.getDatasetForAssetId("provider", new URL("http://fakeUrl:4321/not/working"), "test-asset-id"); - fail("This should not complete without throwing an exception"); + policyServiceSpy.getAcceptablePolicyForAssetId("test-counter-party-id", testUrl, "test-asset-id"); } catch (EdcException expected) { + assertEquals("Could not find any acceptable policyDefinition", expected.getMessage()); } + } - private PolicyServiceConfig mockConfig() { - return new PolicyServiceConfig(ConfigFactory.fromMap(Map.of( - "edc.dsp.callback.address", "http://localhost:4321/dsp", - "web.http.port", "8080", - "web.http.path", "/api"))); + @Test + void getAcceptablePolicyForAssetIdAcceptAllOffersTest() { + //TODO + } + + @Test + void getAcceptablePolicyForAssetIdAcceptFromAcceptedListTest() { + //TODO + } + @Test + void getAcceptablePolicyForAssetIdNoAcceptablePolicyTest() { + //TODO + } + + @Test + void getAcceptablePolicyForAssetIdTimeoutTest() { + //TODO + } + + @Test + void getAcceptablePolicyForAssetIdExceptionbyGetDatasetTest() { + //TODO } } From 55f964812c10bf5b4d7b8ee99ae6b92e952e5db6 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:28:04 +0100 Subject: [PATCH 08/24] Add policy service config test --- .../policy/PolicyServiceConfigTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java new file mode 100644 index 00000000..036bcce7 --- /dev/null +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java @@ -0,0 +1,44 @@ +package de.fraunhofer.iosb.client.policy; + +import org.eclipse.edc.spi.system.configuration.Config; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PolicyServiceConfigTest { + private PolicyServiceConfig policyServiceConfig; + private Config config; + + @BeforeEach + public void initialize() { + config = mock(Config.class); + policyServiceConfig = new PolicyServiceConfig(config); + } + + @Test + public void getWaitForCatalogTimeoutTest() { + var expected = 42; + when(config.getInteger("waitForCatalogTimeout", 10)).thenReturn(expected); + + assertEquals(expected, policyServiceConfig.getWaitForCatalogTimeout()); + } + + @Test + public void getAcceptedPolicyDefinitionsPathTest() { + var expected = "/tmp/test/policy-definitions/accepted/"; + when(config.getString("acceptedPolicyDefinitionsPath", null)).thenReturn(expected); + + assertEquals(expected, policyServiceConfig.getAcceptedPolicyDefinitionsPath()); + } + + @Test + public void isAcceptAllProviderOffersTest() { + var expected = true; + when(config.getBoolean("acceptedPolicyDefinitionsPath", false)).thenReturn(expected); + + assertEquals(expected, policyServiceConfig.isAcceptAllProviderOffers()); + } +} From b8cafa1a1bb5b222ce9ec6b846ecdc46e9737d5a Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:28:12 +0100 Subject: [PATCH 09/24] Code cleanup --- .../iosb/client/ClientEndpoint.java | 11 ++++-- .../iosb/client/ClientExtension.java | 2 +- .../CustomAuthenticationRequestFilter.java | 10 +++--- .../DataTransferController.java | 5 ++- .../DataTransferEndpoint.java | 8 +++-- .../DataTransferObservable.java | 2 +- .../TransferInitiator.java | 4 +-- .../client/policy/PolicyDefinitionStore.java | 6 +++- .../iosb/client/policy/PolicyService.java | 2 +- .../iosb/client/ClientEndpointTest.java | 6 ++-- .../TransferInitiatorTest.java | 8 +++-- .../client/negotiation/NegotiatorTest.java | 6 ++-- .../iosb/client/policy/PolicyServiceTest.java | 25 +++++++++---- .../{testUtils => testutils}/FileManager.java | 6 ++-- .../de/fraunhofer/iosb/app/AasExtension.java | 35 +++++++++---------- .../java/de/fraunhofer/iosb/app/Endpoint.java | 9 ++++- .../java/de/fraunhofer/iosb/app/Logger.java | 4 +-- .../de/fraunhofer/iosb/app/aas/AasAgent.java | 29 +++++++-------- .../iosb/app/aas/FaaastServiceManager.java | 3 +- .../CustomAuthenticationRequestFilter.java | 7 ++-- .../controller/ConfigurationController.java | 12 +++---- .../iosb/app/edc/ResourceHandler.java | 2 +- .../iosb/app/model/aas/AASElement.java | 5 ++- .../aas/CustomAssetAdministrationShell.java | 8 ++++- ...omAssetAdministrationShellEnvironment.java | 4 +-- .../model/aas/CustomConceptDescription.java | 7 ++++ .../iosb/app/model/aas/CustomSemanticId.java | 27 ++++++++++---- .../app/model/aas/CustomSemanticIdKey.java | 19 ++++++++-- .../iosb/app/model/aas/CustomSubmodel.java | 7 +++- .../app/model/aas/CustomSubmodelElement.java | 10 ++++-- .../iosb/app/model/aas/Identifier.java | 7 ++++ .../model/configuration/Configuration.java | 4 +-- .../iosb/app/sync/Synchronizer.java | 6 +++- .../de/fraunhofer/iosb/app/util/AASUtil.java | 7 +++- .../iosb/app/util/HttpRestClient.java | 7 +++- .../fraunhofer/iosb/app/AasExtensionTest.java | 4 ++- .../de/fraunhofer/iosb/app/EndpointTest.java | 31 ++++++++-------- .../fraunhofer/iosb/app/aas/AasAgentTest.java | 27 ++++++-------- .../app/aas/FaaastServiceManagerTest.java | 20 +++++------ ...CustomAuthenticationRequestFilterTest.java | 27 +++++++------- .../iosb/app/edc/ContractHandlerTest.java | 10 +++--- .../iosb/app/sync/SynchronizerTest.java | 35 +++++++++---------- .../{testUtils => testutils}/FileManager.java | 6 ++-- 43 files changed, 286 insertions(+), 194 deletions(-) rename client/src/main/java/de/fraunhofer/iosb/client/{dataTransfer => datatransfer}/DataTransferController.java (98%) rename client/src/main/java/de/fraunhofer/iosb/client/{dataTransfer => datatransfer}/DataTransferEndpoint.java (92%) rename client/src/main/java/de/fraunhofer/iosb/client/{dataTransfer => datatransfer}/DataTransferObservable.java (98%) rename client/src/main/java/de/fraunhofer/iosb/client/{dataTransfer => datatransfer}/TransferInitiator.java (97%) rename client/src/test/java/de/fraunhofer/iosb/client/{dataTransfer => datatransfer}/TransferInitiatorTest.java (93%) rename client/src/test/java/de/fraunhofer/iosb/client/{testUtils => testutils}/FileManager.java (86%) rename edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/{testUtils => testutils}/FileManager.java (86%) 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 fa9acfe4..448ee6ba 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java @@ -15,11 +15,18 @@ */ package de.fraunhofer.iosb.client; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +import de.fraunhofer.iosb.client.datatransfer.DataTransferController; import de.fraunhofer.iosb.client.negotiation.NegotiationController; import de.fraunhofer.iosb.client.policy.PolicyController; import de.fraunhofer.iosb.client.util.Pair; -import jakarta.ws.rs.*; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; 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 45f744ec..57db855d 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,7 @@ */ package de.fraunhofer.iosb.client; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +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.api.auth.spi.AuthenticationService; diff --git a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java b/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java index 01333a40..65b93202 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java @@ -16,7 +16,7 @@ package de.fraunhofer.iosb.client.authentication; import de.fraunhofer.iosb.client.ClientEndpoint; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferEndpoint; +import de.fraunhofer.iosb.client.datatransfer.DataTransferEndpoint; import jakarta.ws.rs.container.ContainerRequestContext; import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter; import org.eclipse.edc.api.auth.spi.AuthenticationService; @@ -64,10 +64,10 @@ public void filter(ContainerRequestContext 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))) { + 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); 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 similarity index 98% rename from client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferController.java rename to client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferController.java index ec3a7377..54fdb207 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 @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.dataTransfer; +package de.fraunhofer.iosb.client.datatransfer; import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; import org.eclipse.edc.api.auth.spi.AuthenticationService; @@ -80,8 +80,7 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ * @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. + * @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 */ diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java similarity index 92% rename from client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java rename to client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java index 3637ebe3..c9ef229c 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.dataTransfer; +package de.fraunhofer.iosb.client.datatransfer; import de.fraunhofer.iosb.client.ClientEndpoint; -import jakarta.ws.rs.*; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.edc.spi.monitor.Monitor; diff --git a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java similarity index 98% rename from client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java rename to client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java index d3ac513a..a9b5ff27 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/DataTransferObservable.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.dataTransfer; +package de.fraunhofer.iosb.client.datatransfer; import org.eclipse.edc.spi.monitor.Monitor; 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 similarity index 97% rename from client/src/main/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiator.java rename to client/src/main/java/de/fraunhofer/iosb/client/datatransfer/TransferInitiator.java index b469abd2..98aa794c 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 @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.dataTransfer; +package de.fraunhofer.iosb.client.datatransfer; import de.fraunhofer.iosb.client.ClientEndpoint; import jakarta.ws.rs.core.UriBuilder; @@ -30,7 +30,7 @@ import java.util.Objects; import java.util.UUID; -import static de.fraunhofer.iosb.client.dataTransfer.DataTransferController.DATA_TRANSFER_API_KEY; +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; diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java index 762ccaa5..6e8f8b21 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyDefinitionStore.java @@ -21,7 +21,11 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import static java.lang.String.format; diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java index 8491e3a8..5f3ebef3 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyService.java @@ -69,7 +69,7 @@ class PolicyService { * @param catalogService Fetching the catalog of a provider. * @param transformer Transform json-ld byte-array catalog to catalog class */ - public PolicyService(CatalogService catalogService, TypeTransformerRegistry transformer, + PolicyService(CatalogService catalogService, TypeTransformerRegistry transformer, PolicyServiceConfig config, PolicyDefinitionStore policyDefinitionStore, Monitor monitor) { this.catalogService = catalogService; this.transformer = transformer; 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 bf438097..556415af 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java @@ -16,7 +16,7 @@ package de.fraunhofer.iosb.client; import com.fasterxml.jackson.databind.ObjectMapper; -import de.fraunhofer.iosb.client.dataTransfer.DataTransferController; +import de.fraunhofer.iosb.client.datatransfer.DataTransferController; import de.fraunhofer.iosb.client.negotiation.NegotiationController; import de.fraunhofer.iosb.client.policy.PolicyController; import jakarta.ws.rs.core.Response; @@ -184,8 +184,8 @@ public void negotiateContractTest() { .build())) { fail(); } catch (EdcException expected) { - if (!(expected.getCause().getClass().equals(TimeoutException.class) - && expected.getMessage().contains("Agreement"))) { + if (!(expected.getCause().getClass().equals(TimeoutException.class) && + expected.getMessage().contains("Agreement"))) { fail(); // This must fail because of agreement timeout. } } 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 similarity index 93% rename from client/src/test/java/de/fraunhofer/iosb/client/dataTransfer/TransferInitiatorTest.java rename to client/src/test/java/de/fraunhofer/iosb/client/datatransfer/TransferInitiatorTest.java index b2776664..8d3ca270 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 @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.dataTransfer; +package de.fraunhofer.iosb.client.datatransfer; import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; @@ -31,7 +31,11 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.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; public class TransferInitiatorTest { diff --git a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java index d6107c49..d4a8d417 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/negotiation/NegotiatorTest.java @@ -25,8 +25,6 @@ import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.response.StatusResult; -import org.eclipse.edc.spi.system.configuration.Config; -import org.eclipse.edc.spi.system.configuration.ConfigFactory; import org.eclipse.edc.spi.types.domain.agreement.ContractAgreement; import org.eclipse.edc.spi.types.domain.offer.ContractOffer; import org.junit.jupiter.api.BeforeEach; @@ -39,7 +37,9 @@ import java.util.concurrent.Executors; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.when; diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java index 58ddec90..0224d611 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java @@ -16,7 +16,7 @@ package de.fraunhofer.iosb.client.policy; import de.fraunhofer.iosb.client.exception.AmbiguousOrNullException; -import de.fraunhofer.iosb.client.testUtils.FileManager; +import de.fraunhofer.iosb.client.testutils.FileManager; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.DataService; import org.eclipse.edc.catalog.spi.Dataset; @@ -29,7 +29,6 @@ import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.result.Result; -import org.eclipse.edc.spi.system.configuration.ConfigFactory; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.BeforeEach; @@ -40,16 +39,18 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; 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.query.Criterion.criterion; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; /** * We assume here that catalogService does not return null objects as well as null catalogs inside their return value. @@ -141,7 +142,19 @@ void getDatasetTest() throws InterruptedException { when(catalogService.requestCatalog(any(), any(), any(), any())).thenReturn(mockedFuture); - when(typeTransformerRegistry.transform(any(), any())).thenReturn(Result.success(Catalog.Builder.newInstance().dataset(Dataset.Builder.newInstance().id(datasetId).offer(UUID.randomUUID().toString(), Policy.Builder.newInstance().build()).distribution(Distribution.Builder.newInstance().dataService(DataService.Builder.newInstance().build()).format("").build()).build()).build())); + when(typeTransformerRegistry.transform(any(), any())) + .thenReturn(Result.success( + Catalog.Builder.newInstance() + .dataset(Dataset.Builder.newInstance() + .id(datasetId) + .offer(UUID.randomUUID().toString(), Policy.Builder.newInstance().build()) + .distribution( + Distribution.Builder.newInstance() + .dataService(DataService.Builder.newInstance().build()) + .format("") + .build()) + .build()) + .build())); assertEquals(datasetId, policyService.getDatasetForAssetId("provider", testUrl, "test-asset-id").getId()); } diff --git a/client/src/test/java/de/fraunhofer/iosb/client/testUtils/FileManager.java b/client/src/test/java/de/fraunhofer/iosb/client/testutils/FileManager.java similarity index 86% rename from client/src/test/java/de/fraunhofer/iosb/client/testUtils/FileManager.java rename to client/src/test/java/de/fraunhofer/iosb/client/testutils/FileManager.java index 3e7c70ec..997f03af 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/testUtils/FileManager.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/testutils/FileManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.client.testUtils; +package de.fraunhofer.iosb.client.testutils; import org.apache.commons.io.IOUtils; @@ -30,10 +30,10 @@ public class FileManager { private FileManager() { } - private static final File resourcesDirectory = new File("src/test/resources"); + private static final File RESOURCES_DIRECTORY = new File("src/test/resources"); public static String loadResource(String fileName) { - try (FileInputStream x = new FileInputStream(new File(resourcesDirectory, fileName))) { + try (FileInputStream x = new FileInputStream(new File(RESOURCES_DIRECTORY, fileName))) { return IOUtils.toString(x, StandardCharsets.UTF_8); } catch (FileNotFoundException e) { fail("File not found exception on file " + fileName); 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..c6e8effd 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,14 @@ */ 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.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 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,14 +32,12 @@ 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.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** * EDC Extension supporting usage of Asset Administration Shells. @@ -59,7 +58,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; @@ -103,7 +102,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 +120,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/Endpoint.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Endpoint.java index 15672b0e..58adc0d8 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Endpoint.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Endpoint.java @@ -19,7 +19,14 @@ import de.fraunhofer.iosb.app.controller.AasController; import de.fraunhofer.iosb.app.controller.ConfigurationController; import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; -import jakarta.ws.rs.*; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Logger.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Logger.java index f1745232..b52f3087 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Logger.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/Logger.java @@ -15,11 +15,11 @@ */ package de.fraunhofer.iosb.app; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; + import java.util.Objects; import java.util.function.Supplier; -import org.eclipse.edc.spi.monitor.ConsoleMonitor; - /** * Singleton class. * Wrapper for logging with prefix diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/AasAgent.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/AasAgent.java index f1fcb4ce..f9fcc99e 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/AasAgent.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/AasAgent.java @@ -15,25 +15,11 @@ */ package de.fraunhofer.iosb.app.aas; -import static java.lang.String.format; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import org.eclipse.edc.spi.EdcException; - import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; - import de.fraunhofer.iosb.app.Logger; import de.fraunhofer.iosb.app.model.aas.CustomAssetAdministrationShell; import de.fraunhofer.iosb.app.model.aas.CustomAssetAdministrationShellEnvironment; @@ -57,6 +43,18 @@ import io.adminshell.aas.v3.model.impl.DefaultSubmodel; import jakarta.ws.rs.core.Response; import okhttp3.OkHttpClient; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static java.lang.String.format; /** * Communicating with AAS service @@ -149,8 +147,7 @@ public Response deleteModel(URL aasServiceUrl, String element) { * sourceUrl field. * * @param aasServiceUrl AAS service to be updated - * @return AAS model enriched with each elements access URL as string in assetId - * field. + * @return AAS model enriched with each elements access URL as string in assetId field. */ public CustomAssetAdministrationShellEnvironment getAasEnvWithUrls(URL aasServiceUrl, boolean onlySubmodels) throws IOException, DeserializationException { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/FaaastServiceManager.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/FaaastServiceManager.java index 13071166..e06f51b5 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/FaaastServiceManager.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/aas/FaaastServiceManager.java @@ -139,8 +139,7 @@ public URL startService(Path aasModelPath, Path configPath, int port) throws IOE faaastServiceRepository.put(new URL(LOCALHOST_URL + localFaaastServicePort), service); - } catch ( - Exception faaastServiceException) { + } catch (Exception faaastServiceException) { throw new EdcException(FAAAST_SERVICE_EXCEPTION_MESSAGE, faaastServiceException); } return new URL(LOCALHOST_URL + localFaaastServicePort); 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 index 818b6f61..85ecadd0 100644 --- 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 @@ -15,13 +15,12 @@ */ package de.fraunhofer.iosb.app.authentication; -import java.util.Objects; - +import de.fraunhofer.iosb.app.Logger; +import jakarta.ws.rs.container.ContainerRequestContext; 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; +import java.util.Objects; /** * Custom AuthenticationRequestFilter filtering requests that go directly to an diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java index ce5af23f..559095a1 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java @@ -15,23 +15,21 @@ */ package de.fraunhofer.iosb.app.controller; -import java.net.URL; -import java.util.Map; - -import org.eclipse.edc.spi.system.configuration.Config; -import org.eclipse.edc.spi.system.configuration.ConfigFactory; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.json.JsonMapper; - import de.fraunhofer.iosb.app.Logger; import de.fraunhofer.iosb.app.RequestType; import de.fraunhofer.iosb.app.model.configuration.Configuration; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; + +import java.net.URL; +import java.util.Map; /** * Handles requests regarding the application's configuration. diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/edc/ResourceHandler.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/edc/ResourceHandler.java index 61342eae..2e9ffa57 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/edc/ResourceHandler.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/edc/ResourceHandler.java @@ -15,8 +15,8 @@ */ package de.fraunhofer.iosb.app.edc; -import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.connector.dataplane.http.spi.HttpDataAddress; +import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.types.domain.asset.Asset; /** diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/AASElement.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/AASElement.java index 5cdc52ae..10c03d64 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/AASElement.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/AASElement.java @@ -15,12 +15,11 @@ */ package de.fraunhofer.iosb.app.model.aas; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonProperty; - import io.adminshell.aas.v3.model.EmbeddedDataSpecification; +import java.util.List; + /* * Collect common attributes of every AAS element. */ diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShell.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShell.java index 50f52888..207d9350 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShell.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShell.java @@ -18,10 +18,11 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import io.adminshell.aas.v3.model.AssetInformation; import io.adminshell.aas.v3.model.impl.DefaultAssetInformation; +import java.util.Objects; + /** * AAS Model for the self description of the edc */ @@ -58,6 +59,11 @@ public void setAssetInformation(DefaultAssetInformation assetInformation) { this.assetInformation = assetInformation; } + @Override + public int hashCode() { + return Objects.hash(identification, idShort); + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShellEnvironment.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShellEnvironment.java index dacd3f7a..6f9e3d9c 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShellEnvironment.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomAssetAdministrationShellEnvironment.java @@ -27,9 +27,9 @@ public class CustomAssetAdministrationShellEnvironment { protected List assetAdministrationShells = new ArrayList<>(); - protected List submodels= new ArrayList<>(); + protected List submodels = new ArrayList<>(); - protected List conceptDescriptions= new ArrayList<>(); + protected List conceptDescriptions = new ArrayList<>(); public List getAssetAdministrationShells() { return assetAdministrationShells; diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomConceptDescription.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomConceptDescription.java index aeab45ba..4270e5fa 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomConceptDescription.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomConceptDescription.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Objects; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonAutoDetect @@ -43,6 +45,11 @@ public void setIdShort(String idShort) { this.idShort = idShort; } + @Override + public int hashCode() { + return Objects.hash(identification, idShort); + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticId.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticId.java index 4459e986..174c3e58 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticId.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticId.java @@ -1,16 +1,29 @@ +/* + * 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.model.aas; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - +import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.adminshell.aas.v3.model.Key; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; @JsonAutoDetect @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @@ -39,7 +52,7 @@ public CustomSemanticId(List keys) { customSemanticIdKey.setIdType(key.getIdType()); customSemanticIdKey.setType(key.getType()); customSemanticIdKey.setValue(key.getValue()); - + this.keys.add(customSemanticIdKey); } diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticIdKey.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticIdKey.java index cfaa1652..22a588a0 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticIdKey.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSemanticIdKey.java @@ -1,15 +1,28 @@ +/* + * 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.model.aas; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - +import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.adminshell.aas.v3.model.KeyElements; import io.adminshell.aas.v3.model.KeyType; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - @JsonAutoDetect @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @JsonInclude(Include.NON_NULL) diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodel.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodel.java index 89d7017d..839f0461 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodel.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodel.java @@ -18,11 +18,11 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import io.adminshell.aas.v3.model.Constraint; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) @@ -66,6 +66,11 @@ public void setSubmodelElements(List submodelElements) { this.submodelElements = submodelElements; } + @Override + public int hashCode() { + return Objects.hash(identification, idShort); + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodelElement.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodelElement.java index d1f91edb..359e9478 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodelElement.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/CustomSubmodelElement.java @@ -15,15 +15,16 @@ */ package de.fraunhofer.iosb.app.model.aas; -import java.util.List; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; - import io.adminshell.aas.v3.model.Constraint; +import java.util.List; +import java.util.Objects; + @JsonAutoDetect @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @JsonInclude(Include.NON_NULL) @@ -49,6 +50,11 @@ public void setIdShort(String idShort) { this.idShort = idShort; } + @Override + public int hashCode() { + return Objects.hash(idShort); + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/Identifier.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/Identifier.java index 9cfe2c21..8e16e6a5 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/Identifier.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/aas/Identifier.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonAutoDetect public class Identifier { @@ -45,6 +47,11 @@ public void setId(String id) { this.id = id; } + @Override + public int hashCode() { + return Objects.hash(idType, id); + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/configuration/Configuration.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/configuration/Configuration.java index f98e5d12..ff5bd4a7 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/configuration/Configuration.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/model/configuration/Configuration.java @@ -15,11 +15,11 @@ */ package de.fraunhofer.iosb.app.model.configuration; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.net.URL; import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * Singleton class. * The configuration of the application. diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java index a4c06c2a..42429d24 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java @@ -17,7 +17,11 @@ import de.fraunhofer.iosb.app.controller.AasController; import de.fraunhofer.iosb.app.controller.ResourceController; -import de.fraunhofer.iosb.app.model.aas.*; +import de.fraunhofer.iosb.app.model.aas.AASElement; +import de.fraunhofer.iosb.app.model.aas.CustomAssetAdministrationShellEnvironment; +import de.fraunhofer.iosb.app.model.aas.CustomSubmodel; +import de.fraunhofer.iosb.app.model.aas.CustomSubmodelElement; +import de.fraunhofer.iosb.app.model.aas.IdsAssetElement; import de.fraunhofer.iosb.app.model.configuration.Configuration; import de.fraunhofer.iosb.app.model.ids.SelfDescription; import de.fraunhofer.iosb.app.model.ids.SelfDescriptionChangeListener; diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/AASUtil.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/AASUtil.java index 89a6d67a..b70b6f26 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/AASUtil.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/AASUtil.java @@ -15,7 +15,12 @@ */ package de.fraunhofer.iosb.app.util; -import de.fraunhofer.iosb.app.model.aas.*; +import de.fraunhofer.iosb.app.model.aas.CustomAssetAdministrationShellEnvironment; +import de.fraunhofer.iosb.app.model.aas.CustomSemanticId; +import de.fraunhofer.iosb.app.model.aas.CustomSubmodel; +import de.fraunhofer.iosb.app.model.aas.CustomSubmodelElement; +import de.fraunhofer.iosb.app.model.aas.CustomSubmodelElementCollection; +import de.fraunhofer.iosb.app.model.aas.IdsAssetElement; import io.adminshell.aas.v3.model.Submodel; import io.adminshell.aas.v3.model.SubmodelElement; import io.adminshell.aas.v3.model.SubmodelElementCollection; diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/HttpRestClient.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/HttpRestClient.java index 689a32b1..28bffc35 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/HttpRestClient.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/util/HttpRestClient.java @@ -16,7 +16,12 @@ package de.fraunhofer.iosb.app.util; import de.fraunhofer.iosb.app.Logger; -import okhttp3.*; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; import java.io.IOException; import java.net.URL; diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/AasExtensionTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/AasExtensionTest.java index 7e8e5076..a8217576 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/AasExtensionTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/AasExtensionTest.java @@ -35,7 +35,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; @ExtendWith(DependencyInjectionExtension.class) public class AasExtensionTest { diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/EndpointTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/EndpointTest.java index 0621f1fc..f2a22720 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/EndpointTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/EndpointTest.java @@ -15,29 +15,28 @@ */ package de.fraunhofer.iosb.app; -import static java.lang.String.format; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import org.eclipse.edc.spi.system.configuration.ConfigFactory; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import de.fraunhofer.iosb.app.controller.AasController; import de.fraunhofer.iosb.app.controller.ConfigurationController; import de.fraunhofer.iosb.app.model.configuration.Configuration; import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; -import de.fraunhofer.iosb.app.testUtils.FileManager; +import de.fraunhofer.iosb.app.testutils.FileManager; import de.fraunhofer.iosb.app.util.Encoder; import jakarta.ws.rs.core.Response; import okhttp3.OkHttpClient; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Not mocking the controllers this endpoint uses, as the mocking/validation diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/AasAgentTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/AasAgentTest.java index 862409d3..2468f59b 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/AasAgentTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/AasAgentTest.java @@ -15,35 +15,30 @@ */ package de.fraunhofer.iosb.app.aas; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.app.testutils.FileManager; +import io.adminshell.aas.v3.dataformat.DeserializationException; +import okhttp3.OkHttpClient; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; -import de.fraunhofer.iosb.app.testUtils.FileManager; -import io.adminshell.aas.v3.dataformat.DeserializationException; -import okhttp3.OkHttpClient; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; /** * Testing AAS Agent. Using mocked AAS service (HTTP endpoints) */ public class AasAgentTest { - /** - * - */ private static final String HTTP_LOCALHOST_8080 = "http://localhost:8080"; private AasAgent aasAgent; diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/FaaastServiceManagerTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/FaaastServiceManagerTest.java index 926639ec..61cd1980 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/FaaastServiceManagerTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/aas/FaaastServiceManagerTest.java @@ -15,22 +15,21 @@ */ package de.fraunhofer.iosb.app.aas; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import de.fraunhofer.iosb.app.util.HttpRestClient; +import jakarta.ws.rs.core.Response; +import okhttp3.OkHttpClient; +import org.eclipse.edc.spi.EdcException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; -import org.eclipse.edc.spi.EdcException; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import de.fraunhofer.iosb.app.util.HttpRestClient; -import jakarta.ws.rs.core.Response; -import okhttp3.OkHttpClient; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class FaaastServiceManagerTest { @@ -89,7 +88,6 @@ public void stopServiceEmptyRepositoryTest() { try { faaastServiceManager.stopService(new URL("http://does-not-exist.com:1234/aas")); fail("This operation should fail"); - } catch (IllegalArgumentException ignored) { } catch (Exception unexpectedException) { fail(); } 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 index 56635dd1..eb731896 100644 --- 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 @@ -15,6 +15,19 @@ */ package de.fraunhofer.iosb.app.authentication; +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; +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 java.util.Map; +import java.util.Objects; + import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -22,20 +35,6 @@ 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); diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/edc/ContractHandlerTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/edc/ContractHandlerTest.java index 177652aa..6f72fb86 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/edc/ContractHandlerTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/edc/ContractHandlerTest.java @@ -15,6 +15,11 @@ */ package de.fraunhofer.iosb.app.edc; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -22,11 +27,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; -import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - public class ContractHandlerTest { private static final String DEFAULT_CONTRACT_NAME = "DEFAULT_CONTRACT"; diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/sync/SynchronizerTest.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/sync/SynchronizerTest.java index 162f62dc..325756e8 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/sync/SynchronizerTest.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/sync/SynchronizerTest.java @@ -15,19 +15,11 @@ */ package de.fraunhofer.iosb.app.sync; -import static java.lang.String.format; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Objects; - +import de.fraunhofer.iosb.app.controller.AasController; +import de.fraunhofer.iosb.app.controller.ResourceController; +import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; +import de.fraunhofer.iosb.app.testutils.FileManager; +import okhttp3.OkHttpClient; import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; import org.eclipse.edc.spi.EdcException; @@ -39,11 +31,18 @@ import org.mockserver.integration.ClientAndServer; import org.mockserver.matchers.Times; -import de.fraunhofer.iosb.app.controller.AasController; -import de.fraunhofer.iosb.app.controller.ResourceController; -import de.fraunhofer.iosb.app.model.ids.SelfDescriptionRepository; -import de.fraunhofer.iosb.app.testUtils.FileManager; -import okhttp3.OkHttpClient; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; public class SynchronizerTest { diff --git a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testUtils/FileManager.java b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testutils/FileManager.java similarity index 86% rename from edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testUtils/FileManager.java rename to edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testutils/FileManager.java index f1a5a139..f7f0b2bc 100644 --- a/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testUtils/FileManager.java +++ b/edc-extension4aas/src/test/java/de/fraunhofer/iosb/app/testutils/FileManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.fraunhofer.iosb.app.testUtils; +package de.fraunhofer.iosb.app.testutils; import org.apache.commons.io.IOUtils; @@ -30,10 +30,10 @@ public class FileManager { private FileManager() { } - private static final File resourcesDirectory = new File("src/test/resources"); + private static final File RESOURCES_DIRECTORY = new File("src/test/resources"); public static String loadResource(String fileName) { - try (FileInputStream x = new FileInputStream(new File(resourcesDirectory, fileName))) { + try (FileInputStream x = new FileInputStream(new File(RESOURCES_DIRECTORY, fileName))) { return IOUtils.toString(x, StandardCharsets.UTF_8); } catch (FileNotFoundException e) { fail("File not found exception on file " + fileName); From 78b05234357edbe3864bab6999dd1e99d8f3c762 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:47:31 +0100 Subject: [PATCH 10/24] Give controller correct config --- .../iosb/client/ClientExtension.java | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) 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..eabac9e6 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java @@ -15,6 +15,10 @@ */ package de.fraunhofer.iosb.client; +import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; +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.api.auth.spi.AuthenticationService; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; @@ -27,45 +31,40 @@ 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; - - @Override - public void initialize(ServiceExtensionContext context) { - var monitor = context.getMonitor(); - var config = context.getConfig("edc.client"); + @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; - 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, + authenticationService, transferProcessManager); - } + webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, + dataTransferController)); + } } From ab74344931e94650c2a363b09abc2775984b55eb Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:48:23 +0100 Subject: [PATCH 11/24] Cleanup, fix smaller bugs --- .../iosb/client/dataTransfer/DataTransferController.java | 7 ++++--- .../iosb/client/dataTransfer/TransferInitiator.java | 3 +++ .../de/fraunhofer/iosb/client/negotiation/Negotiator.java | 2 -- example/dataspaceconnector-configuration.properties | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) 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..3bd0d2b7 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 @@ -62,7 +62,7 @@ public class DataTransferController { */ public DataTransferController(Monitor monitor, Config config, WebService webService, AuthenticationService authenticationService, TransferProcessManager transferProcessManager) { - this.config = config; + this.config = config.getConfig("edc.client"); this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); this.dataEndpointAuthenticationRequestFilter = new CustomAuthenticationRequestFilter(monitor, authenticationService); @@ -70,6 +70,7 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ this.dataTransferObservable = new DataTransferObservable(monitor); var dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); webService.registerResource(dataTransferEndpoint); + webService.registerResource(dataEndpointAuthenticationRequestFilter); } /** @@ -79,7 +80,7 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ * @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 + * @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 @@ -112,7 +113,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..1f407003 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 @@ -73,6 +73,7 @@ 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") .assetId(assetId) @@ -88,6 +89,8 @@ void initiateTransferProcess(URL providerUrl, String agreementId, String assetId private URI createOwnUriFromConfigurationValues(Config config) { var protocolAddressString = config.getString("edc.dsp.callback.address", null); + // Remove /dsp from URL + protocolAddressString = protocolAddressString.substring(0, protocolAddressString.length() - "/dsp".length()); var ownPort = config.getInteger("web.http.port", -1); var ownPath = config.getString("web.http.path", null); try { 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/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 From 93909e37b3f3a5b697065005acb9264da0deaa04 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:53:45 +0100 Subject: [PATCH 12/24] Temporary fix of multiple AuthRequestFilters --- .../CustomAuthenticationRequestFilter.java | 6 +++--- .../CustomAuthenticationRequestFilter.java | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java b/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java index 63b8a74e..49ec7a73 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/authentication/CustomAuthenticationRequestFilter.java @@ -64,9 +64,9 @@ public void filter(ContainerRequestContext 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( + 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)); 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 index 818b6f61..23d5e05b 100644 --- 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 @@ -17,6 +17,7 @@ import java.util.Objects; +import org.apache.commons.lang3.ArrayUtils; import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter; import org.eclipse.edc.api.auth.spi.AuthenticationService; @@ -35,9 +36,10 @@ public class CustomAuthenticationRequestFilter extends AuthenticationRequestFilt public CustomAuthenticationRequestFilter(AuthenticationService authenticationService, String... acceptedEndpoints) { super(authenticationService); if (Objects.nonNull(acceptedEndpoints)) { - endpoints = acceptedEndpoints; + // TODO see below + endpoints = ArrayUtils.addAll(acceptedEndpoints, new String[]{"automated"}); } else { - endpoints = new String[0]; + endpoints = new String[]{"automated"}; } } @@ -51,7 +53,8 @@ public void filter(ContainerRequestContext requestContext) { var requestPath = requestContext.getUriInfo().getPath(); for (String endpoint : endpoints) { - if (Objects.nonNull(endpoint) && endpoint.equalsIgnoreCase(requestPath)) { + // TODO made this "insecure". Fix by creating extension which manages authRequestFilters + if (Objects.nonNull(endpoint) && requestPath.startsWith(endpoint)) { LOGGER.debug( "CustomAuthenticationRequestFilter: Not intercepting this request to an open endpoint"); return; From a06946dc7eac01b6b36a5194ab94ae693e64a1d8 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 16 Mar 2024 17:40:30 +0100 Subject: [PATCH 13/24] Add unified auth request filter Inside public-api-management extension --- 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 +- 10 files changed, 616 insertions(+), 1 deletion(-) 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/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") From 16df19016b3e5f4386ea902f05ecbf171ea6b82e Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 16 Mar 2024 17:41:39 +0100 Subject: [PATCH 14/24] 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 | 9 +- ....java => DataTransferEndpointManager.java} | 51 +++----- .../dataTransfer/DataTransferController.java | 49 ++++---- .../dataTransfer/TransferInitiator.java | 51 +++++--- .../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 | 66 ---------- ...CustomAuthenticationRequestFilterTest.java | 114 ------------------ 13 files changed, 139 insertions(+), 320 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 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 eabac9e6..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,11 +15,10 @@ */ package de.fraunhofer.iosb.client; -import de.fraunhofer.iosb.client.authentication.CustomAuthenticationRequestFilter; +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.api.auth.spi.AuthenticationService; 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; @@ -33,8 +32,10 @@ public class ClientExtension implements ServiceExtension { + // Non-public unified authentication request filter management service @Inject - private AuthenticationService authenticationService; + private PublicApiManagementService publicApiManagementService; + @Inject private CatalogService catalogService; @Inject @@ -61,7 +62,7 @@ public void initialize(ServiceExtensionContext context) { contractNegotiationObservable, contractNegotiationStore, config); var dataTransferController = new DataTransferController(monitor, context.getConfig(), webService, - authenticationService, transferProcessManager); + 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 49ec7a73..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 3bd0d2b7..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,57 +45,53 @@ 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) { + PublicApiManagementService publicApiManagementService, TransferProcessManager transferProcessManager, String connectorId) { this.config = config.getConfig("edc.client"); - this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); - this.dataEndpointAuthenticationRequestFilter = new CustomAuthenticationRequestFilter(monitor, - authenticationService); - + 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); - webService.registerResource(dataEndpointAuthenticationRequestFilter); } /** * 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 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. + * 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); 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 1f407003..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) { @@ -75,7 +77,7 @@ void initiateTransferProcess(URL providerUrl, String agreementId, String assetId .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) @@ -88,11 +90,22 @@ void initiateTransferProcess(URL providerUrl, String agreementId, String assetId } private URI createOwnUriFromConfigurationValues(Config config) { - var protocolAddressString = config.getString("edc.dsp.callback.address", 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()); - var ownPort = config.getInteger("web.http.port", -1); - var ownPath = config.getString("web.http.path", null); try { return UriBuilder .fromUri(protocolAddressString) @@ -107,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/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 23d5e05b..00000000 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/authentication/CustomAuthenticationRequestFilter.java +++ /dev/null @@ -1,66 +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.apache.commons.lang3.ArrayUtils; -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)) { - // TODO see below - endpoints = ArrayUtils.addAll(acceptedEndpoints, new String[]{"automated"}); - } else { - endpoints = new String[]{"automated"}; - } - } - - /** - * 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) { - // TODO made this "insecure". Fix by creating extension which manages authRequestFilters - if (Objects.nonNull(endpoint) && requestPath.startsWith(endpoint)) { - 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); - } -} From 8efce1ed713e8226eff752a61dc37bf9d46241e1 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Sat, 16 Mar 2024 18:16:09 +0100 Subject: [PATCH 15/24] Clean up code --- .../java/de/fraunhofer/iosb/client/ClientExtension.java | 4 ++-- .../authentication/DataTransferEndpointManager.java | 9 --------- .../iosb/client/datatransfer/DataTransferController.java | 5 ++--- .../iosb/client/datatransfer/TransferInitiator.java | 4 +--- .../de/fraunhofer/iosb/client/ClientEndpointTest.java | 2 +- .../fraunhofer/iosb/client/policy/PolicyServiceTest.java | 2 +- .../iosb/app/controller/ConfigurationController.java | 2 +- .../java/de/fraunhofer/iosb/app/sync/Synchronizer.java | 2 +- 8 files changed, 9 insertions(+), 21 deletions(-) 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 1dd98e64..4ed6044b 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java @@ -15,10 +15,10 @@ */ package de.fraunhofer.iosb.client; +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 de.fraunhofer.iosb.api.PublicApiManagementService; 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; @@ -62,7 +62,7 @@ public void initialize(ServiceExtensionContext context) { contractNegotiationObservable, contractNegotiationStore, config); var dataTransferController = new DataTransferController(monitor, config, webService, - publicApiManagementService, transferProcessManager, context.getConnectorId()); + publicApiManagementService, transferProcessManager); webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, dataTransferController)); diff --git a/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java b/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java index 72e26447..6ad4efcc 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/authentication/DataTransferEndpointManager.java @@ -19,18 +19,9 @@ 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; -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.List; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -import static java.lang.String.format; /** * Custom AuthenticationRequestFilter filtering requests that go directly to an 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 96120515..c4af412c 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 @@ -58,12 +58,11 @@ public class DataTransferController { * 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, - PublicApiManagementService publicApiManagementService, TransferProcessManager transferProcessManager, String connectorId) { + PublicApiManagementService publicApiManagementService, TransferProcessManager transferProcessManager) { this.config = config.getConfig("edc.client"); - this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager, connectorId); + this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); this.dataTransferEndpointManager = new DataTransferEndpointManager(publicApiManagementService); this.dataTransferObservable = new DataTransferObservable(monitor); var dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); 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 6c960c86..3ad3ecd0 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 @@ -43,14 +43,12 @@ 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, String connectorId) { + TransferProcessManager transferProcessManager) { this.monitor = monitor; this.ownUri = createOwnUriFromConfigurationValues(config); this.transferProcessManager = transferProcessManager; - this.connectorId = connectorId; } void initiateTransferProcess(URL providerUrl, String agreementId, String assetId, String apiKey) { 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 f910c557..1bf6780d 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/ClientEndpointTest.java @@ -116,7 +116,7 @@ public void setup() throws IOException { mockConfig(), mock(WebService.class), mock(PublicApiManagementService.class), - mockTransferProcessManager(), "")); + mockTransferProcessManager())); } private Config mockConfig() { diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java index 0224d611..40694f4a 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceTest.java @@ -59,7 +59,6 @@ */ public class PolicyServiceTest { - private final int providerPort = 54321; private CatalogService catalogService; private TypeTransformerRegistry typeTransformerRegistry; private PolicyServiceConfig config; @@ -67,6 +66,7 @@ public class PolicyServiceTest { private final URL testUrl; public PolicyServiceTest() throws MalformedURLException { + int providerPort = 54321; testUrl = new URL("http://localhost:" + providerPort); } diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java index 559095a1..60345873 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java @@ -86,7 +86,7 @@ private Response updateConfiguration(String newConfigValues) { // Read config values as map -> edc Config -> merge with old // -> set as AAS extension config Config newConfig = ConfigFactory.fromMap(objectMapper.readValue(newConfigValues, - new TypeReference>() { + new TypeReference<>() { })); Config mergedConfig = sysConfig.merge(newConfig); configuration = objectReader.readValue(objectMapper.writeValueAsString(mergedConfig.getEntries())); diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java index 42429d24..a14a1397 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/sync/Synchronizer.java @@ -157,7 +157,7 @@ private void syncSubmodel(CustomAssetAdministrationShellEnvironment newEnvironme oldSubmodel = oldSubmodels.get(oldSubmodels.indexOf(submodel)); } else { oldSubmodel = oldSubmodels.stream().filter( - oldSubmodelTest -> submodel.equals(oldSubmodelTest)) + submodel::equals) .findFirst().orElse(null); if (Objects.isNull(oldSubmodel)) { return; From 1bf45c7b54e86cbcf9970631ec9a261a2de4fffe Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:24:30 +0100 Subject: [PATCH 16/24] Fix repeated negotiation --- .../de/fraunhofer/iosb/client/negotiation/Negotiator.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 c95ae59f..822d10c6 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 @@ -61,7 +61,12 @@ StatusResult negotiate(ContractRequest contractRequest) { if (!relevantAgreements.isEmpty()) { // assuming contractNegotiationStore removes invalid agreements return StatusResult.success( - ContractNegotiation.Builder.newInstance().contractAgreement(relevantAgreements.get(0)).build()); + ContractNegotiation.Builder.newInstance() + .contractAgreement(relevantAgreements.get(0)) + .counterPartyAddress(contractRequest.getCounterPartyAddress()) + .counterPartyId(contractRequest.getProviderId()) + .protocol(contractRequest.getProtocol()) + .build()); } return consumerNegotiationManager.initiate(contractRequest); From 1438cdc7d7bf7ccf82d8f3162eb0ead98df5551e Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:24:49 +0100 Subject: [PATCH 17/24] Fix repeated negotiation timeout while waiting --- .../client/negotiation/NegotiationController.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java index f5286247..3d34c93b 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/negotiation/NegotiationController.java @@ -24,6 +24,7 @@ import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.types.domain.agreement.ContractAgreement; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -60,14 +61,18 @@ public ContractAgreement negotiateContract(ContractRequest contractRequest) throws InterruptedException, ExecutionException { var negotiationStatusResult = negotiator.negotiate(contractRequest); - - if (negotiationStatusResult.succeeded()) { - return waitForAgreement(negotiationStatusResult.getContent().getId()); - } else { + if (!negotiationStatusResult.succeeded()) { throw new EdcException(negotiationStatusResult.getFailureDetail()); } + var negotiation = negotiationStatusResult.getContent(); + if (Objects.nonNull(negotiation.getContractAgreement())) { + return negotiationStatusResult.getContent().getContractAgreement(); + } else { + return waitForAgreement(negotiation.getId()); + } } + private ContractAgreement waitForAgreement(String negotiationId) throws InterruptedException, ExecutionException { var agreementFuture = new CompletableFuture(); var timeout = config.getInteger("waitForAgreementTimeout", From f5837bc30bf45b8d96d6a76c2fb593187e6fc140 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:25:02 +0100 Subject: [PATCH 18/24] Fix wrong config value read --- .../de/fraunhofer/iosb/client/policy/PolicyServiceConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfig.java b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfig.java index 0f4fc2b7..993f9cd6 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfig.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfig.java @@ -33,7 +33,7 @@ public PolicyServiceConfig(Config config) { } boolean isAcceptAllProviderOffers() { - return config.getBoolean("acceptedPolicyDefinitionsPath", ACCEPT_ALL_POLICY_DEFINITIONS_DEFAULT); + return config.getBoolean("acceptAllProviderOffers", ACCEPT_ALL_POLICY_DEFINITIONS_DEFAULT); } int getWaitForCatalogTimeout() { From 701ba0e91cc824cf201eeec9d0332f1dc84f5246 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:25:17 +0100 Subject: [PATCH 19/24] Fix wrong config passed --- .../main/java/de/fraunhofer/iosb/client/ClientExtension.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4ed6044b..d9d547b0 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientExtension.java @@ -61,7 +61,8 @@ public void initialize(ServiceExtensionContext context) { var negotiationController = new NegotiationController(consumerNegotiationManager, contractNegotiationObservable, contractNegotiationStore, config); - var dataTransferController = new DataTransferController(monitor, config, webService, + // This controller needs base config to read EDC's hostname + specific ports + var dataTransferController = new DataTransferController(monitor, context.getConfig(), webService, publicApiManagementService, transferProcessManager); webService.registerResource(new ClientEndpoint(monitor, negotiationController, policyController, From 7b481dbb26d90dc7f422fd1359fb99d73be0ee1c Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:26:09 +0100 Subject: [PATCH 20/24] Cleanup code, sanitize http responses --- .../iosb/client/ClientEndpoint.java | 59 ++++++++++++++++--- .../datatransfer/DataTransferController.java | 7 ++- .../datatransfer/DataTransferEndpoint.java | 6 +- .../datatransfer/DataTransferObservable.java | 7 ++- .../datatransfer/TransferInitiatorTest.java | 2 +- .../controller/ConfigurationController.java | 1 - example/build.gradle.kts | 2 - 7 files changed, 62 insertions(+), 22 deletions(-) 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 1cba602b..e5eaca6c 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/ClientEndpoint.java @@ -15,10 +15,13 @@ */ package de.fraunhofer.iosb.client; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import de.fraunhofer.iosb.client.datatransfer.DataTransferController; import de.fraunhofer.iosb.client.negotiation.NegotiationController; import de.fraunhofer.iosb.client.policy.PolicyController; import de.fraunhofer.iosb.client.util.Pair; +import jakarta.json.*; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -29,16 +32,21 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.catalog.spi.Dataset; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.types.domain.agreement.ContractAgreement; import org.eclipse.edc.spi.types.domain.offer.ContractOffer; +import java.io.StringReader; import java.net.URL; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import static java.lang.String.format; import static org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; @@ -55,7 +63,7 @@ public class ClientEndpoint { */ public static final String AUTOMATED_PATH = "automated"; private static final String ACCEPTED_POLICIES_PATH = "acceptedPolicies"; - private static final String DATASET_PATH = "dataset"; + private static final String OFFER_PATH = "offer"; private static final String NEGOTIATE_CONTRACT_PATH = "negotiateContract"; private static final String NEGOTIATE_PATH = "negotiate"; private static final String TRANSFER_PATH = "transfer"; @@ -66,6 +74,8 @@ public class ClientEndpoint { private final PolicyController policyController; private final DataTransferController transferController; + private final ObjectMapper objectMapper; + /** * Initialize a client endpoint. * @@ -83,22 +93,23 @@ public ClientEndpoint(Monitor monitor, this.policyController = policyController; this.negotiationController = negotiationController; this.transferController = transferController; + this.objectMapper = new ObjectMapper(); } /** - * Return dataset for assetId that match any policyDefinitions' policy + * Return dataset for assetId that match any policyDefinition's policy * 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. + * @return A dataset offered by the provider for the given assetId. */ @GET - @Path(DATASET_PATH) - public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryParam("assetId") String assetId, @QueryParam("providerId") String counterPartyId) { - monitor.debug(format("[Client] Received a %s GET request", DATASET_PATH)); + @Path(OFFER_PATH) + public Response getOffer(@QueryParam("providerUrl") URL providerUrl, @QueryParam("assetId") String assetId, @QueryParam("providerId") String counterPartyId) { + monitor.debug(format("[Client] Received a %s GET request", OFFER_PATH)); if (Objects.isNull(providerUrl)) { return Response.status(Response.Status.BAD_REQUEST).entity("Provider URL must not be null").build(); @@ -106,15 +117,40 @@ public Response getDataset(@QueryParam("providerUrl") URL providerUrl, @QueryPar try { var dataset = policyController.getDataset(counterPartyId, providerUrl, assetId); - return Response.ok(dataset).build(); + + var parsedResponse = buildResponseFrom(dataset); + return Response.ok(parsedResponse).build(); + } catch (InterruptedException interruptedException) { - monitor.severe(format("[Client] Getting dataset failed for provider %s and asset %s", providerUrl, + monitor.severe(format("[Client] Getting offer failed for provider %s and asset %s", providerUrl, assetId), interruptedException); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(interruptedException.getMessage()) .build(); + + } catch (JsonProcessingException policyWriteException) { + monitor.severe(format("[Client] Parsing policy failed for provider %s and asset %s", providerUrl, + assetId), policyWriteException); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(policyWriteException.getMessage()) + .build(); + } } + private String buildResponseFrom(Dataset dataset) throws JsonProcessingException { + var offer = dataset.getOffers().entrySet().stream().findFirst().orElseThrow(); + + // Build negotiation request body for the user + var policyString = objectMapper.writeValueAsString(offer.getValue()); + var policyJson = Json.createReader(new StringReader(policyString)).read(); + + return Json.createObjectBuilder() + .add("id", offer.getKey()) + .add("policy", policyJson) + .add("assetId", offer.getValue().getTarget()) + .build() + .toString(); + } + /** * Negotiate a contract agreement using the given contract offer if no agreement * exists for this constellation. @@ -186,7 +222,12 @@ public Response negotiateContract(ContractRequest contractRequest) { Objects.requireNonNull(contractRequest, "ContractRequest must not be null"); try { var agreement = negotiationController.negotiateContract(contractRequest); - return Response.ok(agreement).build(); + // Sanitize response (only ID is relevant here) + var agreementResponse = Json.createObjectBuilder() + .add("agreement-id", agreement.getId()) + .build() + .toString(); + return Response.ok(agreementResponse).build(); } catch (InterruptedException | ExecutionException negotiationException) { monitor.severe( format("[Client] Negotiation failed for provider %s and contractRequest %s", 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 c4af412c..995d66a3 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 @@ -61,8 +61,8 @@ public class DataTransferController { */ public DataTransferController(Monitor monitor, Config config, WebService webService, PublicApiManagementService publicApiManagementService, TransferProcessManager transferProcessManager) { - this.config = config.getConfig("edc.client"); this.transferInitiator = new TransferInitiator(config, monitor, transferProcessManager); + this.config = config.getConfig("edc.client"); this.dataTransferEndpointManager = new DataTransferEndpointManager(publicApiManagementService); this.dataTransferObservable = new DataTransferObservable(monitor); var dataTransferEndpoint = new DataTransferEndpoint(monitor, dataTransferObservable); @@ -85,8 +85,7 @@ public DataTransferController(Monitor monitor, Config config, WebService webServ public String initiateTransferProcess(URL providerUrl, String agreementId, String assetId, URL dataDestinationUrl) throws InterruptedException, ExecutionException { // Prepare for incoming data - var dataFuture = new CompletableFuture(); - dataTransferObservable.register(dataFuture, agreementId); + var dataFuture = dataTransferObservable.register(agreementId); if (Objects.isNull(dataDestinationUrl)) { var apiKey = UUID.randomUUID().toString(); @@ -95,11 +94,13 @@ public String initiateTransferProcess(URL providerUrl, String agreementId, Strin this.transferInitiator.initiateTransferProcess(providerUrl, agreementId, assetId, apiKey); return waitForData(dataFuture, agreementId); } else { + // Send data to custom target url var dataSinkAddress = HttpDataAddress.Builder.newInstance() .baseUrl(dataDestinationUrl.toString()) .build(); this.transferInitiator.initiateTransferProcess(providerUrl, agreementId, assetId, dataSinkAddress); + // Don't have to wait for data return null; } diff --git a/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java index c9ef229c..441ca47d 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferEndpoint.java @@ -40,14 +40,14 @@ public class DataTransferEndpoint { /* * Path for providers to send data to. */ - public static final String RECEIVE_DATA_PATH = "receiveData"; + static final String RECEIVE_DATA_PATH = "receiveData"; private final Monitor monitor; private final DataTransferObservable observable; - public DataTransferEndpoint(Monitor monitor, DataTransferObservable observable) { + DataTransferEndpoint(Monitor monitor, DataTransferObservable dataTransferObservable) { this.monitor = monitor; - this.observable = observable; + this.observable = dataTransferObservable; } /** diff --git a/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java index a9b5ff27..1423f57b 100644 --- a/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java +++ b/client/src/main/java/de/fraunhofer/iosb/client/datatransfer/DataTransferObservable.java @@ -40,11 +40,12 @@ class DataTransferObservable { /** * Register a future that should complete if a data transfer is finished. * - * @param observer The future to complete if data transfer is finished. * @param agreementId The agreement ID this future is dependent on. + * @return Object containing data in case of transfer. */ - void register(CompletableFuture observer, String agreementId) { - observers.put(agreementId, observer); + CompletableFuture register(String agreementId) { + observers.put(agreementId, new CompletableFuture()); + return observers.get(agreementId); } /** 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 8c0c0a16..8d3ca270 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 @@ -50,7 +50,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, "http://localhost"); + transferInitiator = new TransferInitiator(configMock, mock(Monitor.class), mockTransferProcessManager); mockStatusResult = (StatusResult) mock(StatusResult.class); diff --git a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java index 60345873..736526f6 100644 --- a/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java +++ b/edc-extension4aas/src/main/java/de/fraunhofer/iosb/app/controller/ConfigurationController.java @@ -29,7 +29,6 @@ import org.eclipse.edc.spi.system.configuration.ConfigFactory; import java.net.URL; -import java.util.Map; /** * Handles requests regarding the application's configuration. diff --git a/example/build.gradle.kts b/example/build.gradle.kts index 9c20a1a8..4153357d 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -38,10 +38,8 @@ dependencies { implementation("$group:data-plane-core:$edcVersion") implementation("$group:data-plane-http:$edcVersion") implementation("$group:data-plane-client:$edcVersion") - implementation("$group:data-plane-selector-client:$edcVersion") implementation("$group:data-plane-selector-core:$edcVersion") implementation("$group:transfer-data-plane:$edcVersion") - } From 033bc5c13c141958ed7ffefd1627c6ae704b3df4 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:26:24 +0100 Subject: [PATCH 21/24] Update readme: client negotiation --- example/README.md | 40 +++++++++++------------ example/resources/tutorial-images/step-2 | Bin 0 -> 107564 bytes 2 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 example/resources/tutorial-images/step-2 diff --git a/example/README.md b/example/README.md index 77b93181..83a7124a 100644 --- a/example/README.md +++ b/example/README.md @@ -113,10 +113,10 @@ java -Dedc.fs.config=./example/configurations/consumer.properties -jar ./example ``` Starting the data transfer from provider to consumer. There is a `postman collection` containing the necessary http -request located in `/examples/resources`. Do the following steps: +requests located in `/examples/resources`. Complete the following steps: -1. Call the provider's self description on `http://localhost:8181/api/selfDescription`, and choose an element you want - to fetch. Put its `asset id` as a variable in the postman collection's variables section. +1. Call the provider's self-description on `http://localhost:8181/api/selfDescription` and choose an element you want + to fetch. Put its `asset id` as a variable in the postman collection's variables section ("Current value"). ### Fully automated @@ -142,15 +142,16 @@ __Important__: ### Separate requests 1. Execute the request `Client/1. Get dataset for asset` - Choose a policy of the response body. + This will return the provider offer for this asset. Copy the full response for the next step. -2. Put the policy inside of request `Client/2. Initiate negotiation with contractOffer`'s body (policy field) and +2. Paste the response inside of request `Client/2. Initiate negotiation with contractOffer`'s body ("contractOffer" field, as can be seen in the screenshot) and execute said request. + -3. If everything went right, request `2` returns an agreementID. Update the postman collection's agreementID variable - with the response value. +3. If everything succeeded, request `2` returns an agreementID. Update the postman collection's "agreement-id" variable + using the response value. -4. Execute request `3. Get data for agreement id and asset id`. If everything went right, the response should be the +4. Execute request `3. Get data for agreement id and asset id`. If again everything went right, the response should be the data behind the previously selected asset. ## Running the Example (manual) @@ -190,18 +191,17 @@ requests for data transfer in this extensions repository located in `/examples/r 3. With this ``, query the consumer connector about the state of the negotiation. Execute request 2 of the data transfer folder. It should return: - -```json -{ - "contractAgreementId": "", - "counterPartyAddress": "http://localhost:8282/api/v1/ids/data", - "errorDetail": null, - "id": "ac6e1c97-13d6-41ff-8b79-1029d7f094bb", - "protocol": "ids-multipart", - "state": "CONFIRMED", - "type": "CONSUMER" -} -``` + ```json + { + "contractAgreementId": "", + "counterPartyAddress": "http://localhost:8282/api/v1/ids/data", + "errorDetail": null, + "id": "ac6e1c97-13d6-41ff-8b79-1029d7f094bb", + "protocol": "ids-multipart", + "state": "CONFIRMED", + "type": "CONSUMER" + } + ``` 4. Put the `` in the postman collection's agreement-id variable. Execute request 3 of the Data Transfer folder. The provider connector should now send the data, and in the consumer diff --git a/example/resources/tutorial-images/step-2 b/example/resources/tutorial-images/step-2 new file mode 100644 index 0000000000000000000000000000000000000000..7a2664352886665747ab2362989127b29255bfe2 GIT binary patch literal 107564 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%0DWhE84EwNc;pl|7vy0 z{<93yUtVq*_S}0M?2);i?k4n1k$4YR*W+y)=vA*l( z_4P-lZ~jo~``|fSB3<*oRI@f(cVc$3=HWzvG3%xaF5219m7PhI|3|V9jq-R&L5qkr#R zv$H|hJf0{Q9o_C^vb?s9T0~s&NF~@fDiO>A2Lv7%NiNHjwQ6-wSYH{wpGZwb`Lm5W zp>?XLY|5Thq*^AY;$hoUno&fU$%L*2Gj=&G;Kr^H2Ac&M!*(qD8Vt!f=>1!Adtc~K zlo#fb$UIo|M=rIN((DszttyVD3?cHXLLf^k(bf!?#bmy$bpQ@$$sQ9Hz)w8?jH{2u zuB}-@n-<=Gz~31Wrefu6OaGd5Zm+|m^-(_MKOKo{5X#H#{a{u4E_QvRzWw;Do8+20}oUlU(QTS=P; z_zM4vo6A#uj2Ndr;l<4i(?U8w*m(|6MspNQ}*QXP0K~RJ81!UVh=(eFx4SP;Rr9~b{qr~3a>hjm-wDw4l#YVx~M2QFYP7WjBrwlf}x&i3E%9;8@1(w#k3Pj9K@+_mx3KQ~7?H z776Xs!np1{W05yqoVnnex#jt)GlQtWik~kw_W`Me&cfrZ#{UGIXuHedK1dV3s;9qY zUipB;+4^eE$>98snQRC)z6?@dsVCg-{yR4$z5@AS*uISXa!^8j$Y6tm8LvC!VMC+! zyxLn#cdWNI>g{5&7%2zsyH5uBB@XL0R6oClkDT2qC{(9 zgUr@AH6ef$x_kTmLefjd6f~Mq;@UTGpaYS(li>W$j==k!ucEoG6T@n3As$6PcJy`3 zPC4CgfZ+7tX;Q5~2|s1+`=mGrek*g+akS8;eM}vN&}7_GV*{m{?M1?aGd?k2V2}dK z8cNto^BqhrKe;ac5t>d7UvqSMnJKtLb$thM&TODzql@NxwbmZlaQ8MV1>6rQ6H~ds zD%Y%`mWjcn5)XYjZ4ghIR!=Q#%o`r(oM0O4D=JzcaCMjDGC>wOAd)$stg&Vs*($I& z^F<8?AkE;Bx}&Gf4Xb6_zRt^9N*30{Yis}al;YwthfR2a7D8-E?*#r>Ovnz<+&5Ei zK}qQj_hkUhJBTO>O_@n$730-wBE1?*#+Pb2aOl_W6D`xgYmVN7@pTLIg#Pdul%ybl zRkhKk$5817R*zHbh81zvHt4Wiq z#gWq3b9^M@lA4{45Kx<48@13Pu_*Pw^^o<6(`o)y`BX=+YaXVfq5hEuWD|+{qU@wW z`oU9nLQ`4<0U4nF!9h8%N24~h4t63}e-#ukG0$bn+!V_78tOf+Nk__EZ(fgkR_VCdL0EV(V~eQQnxGA!K zz5ynNCO(4=Z!6#`UniD-IC&l!c+x^5_Fj3OZJi29*noFH)@wRK8u$`+o)_X?uyjBvO@+)ZAu1}^ zuqL5z&T{n=f70972S>TWWu)@!@l9D-)hqj)ZBtW}wejzI4KtiqRWa$T0!<;W2EHS?rZG`d}c!pZP1K!e=~6lVI~>n7y>79ZN$j< zI{FwnkKPCplYuy?wVLeq?B^U_+5}n{N`;OCu8rqPL=X^IJNf0rlwndG0P^1SHmW;S zB*dzrwUs-Ib(Wa?QZ1(DfhCo2yoAz4+Fxw+e^u!{ix;H9rg10XhgKd9Z0oFk{cW>; zh>eF`4qHU}Ox5T^D)U!?Ujcuk2L|}^DbsxMhImXb920a!Iy>9UHw}R>e(zdX`P@ab zPq#ZVh&V*JTcV)CXKy%sz{c*STdhWzXz470-QJ z%{9rd^$`d zv{)^X-lgpPqMERqXJvQE8~i(GH-RFG_|?41bcO4*=g4TGAK3F~V0OvfiPR4`fUX7u zca_T+ma{>Kr%twVte#LZ^=Gc9wgVfDNlvz8^^jjp=0bjoY`O|z*z>6RzP1YfRG3@e zTI#n^78=jeMxrE}CF@tjlWl*QyWXfaZuojZD(*#m`D6Q=-MR87Bdmn(iB%`9j*?u6 ze&DOsc|YiTBflz0Urb&$R%O31K7-tSdscZhxHLB<=kDI+OU>wKI%Y|ui4=A#yzm}q zHjQLCkqV=5PkMoCEe-mHU*>8IGw}wiIL>kTkHTXf=wkksnl6NIt1DLynOBm5XQ_j+VipE7xxkC zrl;Z=Ylu{N>zv(!BkHD_7MIujY_j>(+{F!gRjC6GJOz;euo(!LXsjz_jHW!bJ58)}-Wh337EezIau_U&`Fp^>=X#mh zzFG_NqGG&184>#4@$og!NxOR)#`66MO~A)>|0e0J$41F%8+#L;NBa2P^Tx0l$?d_d zK6?_K$ATj}$FL+xu}c-0yWd&d(~Ydw1Q6 z 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 SZOR7sLE@gSelF{r5}E)afn$FF literal 0 HcmV?d00001 From 4956c5f2edec3deba35e4cdc9a828c1fb34907e1 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:59:56 +0100 Subject: [PATCH 22/24] Fix PolicyServiceConfigTest --- .../fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java index a0980d5a..ef26e426 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java @@ -38,7 +38,7 @@ public void getAcceptedPolicyDefinitionsPathTest() { @Test public void isAcceptAllProviderOffersTest() { var expected = true; - when(config.getBoolean("acceptedPolicyDefinitionsPath", false)).thenReturn(expected); + when(config.getBoolean("acceptAllProviderOffers", false)).thenReturn(expected); assertTrue(policyServiceConfig.isAcceptAllProviderOffers()); } From 50649ab870a01b29562bb70277c2394111c3e55e Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:00:30 +0100 Subject: [PATCH 23/24] Add license header --- .../client/policy/PolicyServiceConfigTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java index ef26e426..aa8e4556 100644 --- a/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java +++ b/client/src/test/java/de/fraunhofer/iosb/client/policy/PolicyServiceConfigTest.java @@ -1,3 +1,18 @@ +/* + * 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.client.policy; import org.eclipse.edc.spi.system.configuration.Config; From 55ceb63048d37d275e689567e35d52236f4e54f3 Mon Sep 17 00:00:00 2001 From: Carlos Schmidt <18703981+carlos-schmidt@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:04:59 +0100 Subject: [PATCH 24/24] Update postman collection --- .../aas_edc_extension.postman_collection.json | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/example/resources/aas_edc_extension.postman_collection.json b/example/resources/aas_edc_extension.postman_collection.json index 33420325..25e6c969 100644 --- a/example/resources/aas_edc_extension.postman_collection.json +++ b/example/resources/aas_edc_extension.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "8953f55e-787b-4b76-9c0b-7c1b15731185", + "_postman_id": "14677191-e263-4d83-ba13-d126c064b460", "name": "EDC Extension", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "20511334" @@ -207,19 +207,19 @@ "response": [] }, { - "name": "1. Get dataset for asset", + "name": "1. Get offer for asset", "request": { "method": "GET", "header": [], "url": { - "raw": "{{consumer}}/api/automated/dataset?providerUrl={{provider-dsp}}&assetId={{asset-id}}", + "raw": "{{consumer}}/api/automated/offer?providerUrl={{provider-dsp}}&assetId={{asset-id}}", "host": [ "{{consumer}}" ], "path": [ "api", "automated", - "dataset" + "offer" ], "query": [ { @@ -242,7 +242,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{ \r\n \"@context\": {\r\n \"edc\": \"https://w3id.org/edc/v0.0.1/ns/\",\r\n \"odrl\": \"http://www.w3.org/ns/odrl/2/\"\r\n },\r\n \"providerId\": \"provider\",\r\n \"consumerId\": \"consumer\",\r\n \"connectorAddress\": \"{{provider}}\",\r\n \"protocol\": \"dataspace-protocol-http\",\r\n \"counterPartyId\": \"provider\",\r\n \"counterPartyAddress\": \"{{provider-dsp}}\",\r\n \"contractOffer\": {\r\n \"id\": \"DEFAULT_CONTRACT24:-1268910060:3886d894-3955-4b06-83c6-e86fdeb5e65a\",\r\n \"policy\": {\r\n \"permissions\": [\r\n {\r\n \"edctype\": \"dataspaceconnector:permission\",\r\n \"target\": \"{{asset-id}}\",\r\n \"action\": {\r\n \"type\": \"USE\"\r\n }\r\n }\r\n ],\r\n \"target\": \"{{asset-id}}\"\r\n },\r\n \"assetId\": \"{{asset-id}}\"\r\n }\r\n}", + "raw": "{\r\n \"@context\": {\r\n \"edc\": \"https://w3id.org/edc/v0.0.1/ns/\",\r\n \"odrl\": \"http://www.w3.org/ns/odrl/2/\"\r\n },\r\n \"providerId\": \"provider\",\r\n \"protocol\": \"dataspace-protocol-http\",\r\n \"counterPartyId\": \"provider\",\r\n \"counterPartyAddress\": \"{{provider-dsp}}\",\r\n \"contractOffer\": \r\n}", "options": { "raw": { "language": "json" @@ -267,7 +267,13 @@ "name": "3. Get data for agreement id and asset id", "request": { "method": "GET", - "header": [], + "header": [ + { + "key": "x-api-key", + "value": "password", + "type": "text" + } + ], "url": { "raw": "{{consumer}}/api/automated/transfer?providerUrl={{provider-dsp}}&agreementId={{agreement-id}}&assetId={{asset-id}}", "host": [ @@ -944,6 +950,16 @@ } ], "variable": [ + { + "key": "asset-id", + "value": "", + "type": "default" + }, + { + "key": "agreement-id", + "value": "", + "type": "default" + }, { "key": "provider", "value": "http://localhost:8181", @@ -974,26 +990,6 @@ "value": "http://localhost:9192/management", "type": "default" }, - { - "key": "asset-id", - "value": "", - "type": "default" - }, - { - "key": "contract-offer-id", - "value": "", - "type": "default" - }, - { - "key": "negotiation-id", - "value": "", - "type": "default" - }, - { - "key": "agreement-id", - "value": "", - "type": "default" - }, { "key": "docker-provider-connector-url", "value": "http://provider:8181",