diff --git a/src/main/java/com/metaformsystems/redline/api/controller/EdcDataController.java b/src/main/java/com/metaformsystems/redline/api/controller/EdcDataController.java index a0c751b..b2f9143 100644 --- a/src/main/java/com/metaformsystems/redline/api/controller/EdcDataController.java +++ b/src/main/java/com/metaformsystems/redline/api/controller/EdcDataController.java @@ -9,6 +9,7 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - OpenAPI and file upload * */ @@ -24,10 +25,12 @@ import com.metaformsystems.redline.api.dto.response.FileResource; import com.metaformsystems.redline.domain.service.DataAccessService; import com.metaformsystems.redline.infrastructure.client.management.dto.Catalog; +import com.metaformsystems.redline.infrastructure.client.management.dto.CelExpression; import com.metaformsystems.redline.infrastructure.client.management.dto.Constraint; import com.metaformsystems.redline.infrastructure.client.management.dto.Obligation; import com.metaformsystems.redline.infrastructure.client.management.dto.Offer; import com.metaformsystems.redline.infrastructure.client.management.dto.Permission; +import com.metaformsystems.redline.infrastructure.client.management.dto.PolicySet; import com.metaformsystems.redline.infrastructure.client.management.dto.Prohibition; import com.metaformsystems.redline.infrastructure.client.management.dto.TransferProcess; import io.swagger.v3.oas.annotations.Operation; @@ -82,14 +85,22 @@ public EdcDataController(DataAccessService dataAccessService, ObjectMapper objec public ResponseEntity uploadFile(@PathVariable Long participantId, @PathVariable Long tenantId, @PathVariable Long providerId, - @RequestPart("publicMetadata") String publicMetadata, - @RequestPart("privateMetadata") String privateMetadata, + @RequestPart("publicMetadata") Map publicMetadata, + @RequestPart("privateMetadata") Map privateMetadata, + @RequestPart(value = "celExpressions", required = false) List celExpressions, + @RequestPart(value = "policySet", required = false) PolicySet policySet, @RequestPart("file") MultipartFile file) { - try { - var publicMetadataMap = objectMapper.readValue(publicMetadata, new TypeReference>() {}); - var privateMetadataMap = objectMapper.readValue(privateMetadata, new TypeReference>() {}); - dataAccessService.uploadFileForParticipant(participantId, publicMetadataMap, privateMetadataMap, file.getInputStream(), file.getContentType(), file.getOriginalFilename()); + dataAccessService.uploadFileForParticipant( + participantId, + publicMetadata, + privateMetadata, + file.getInputStream(), + file.getContentType(), + file.getOriginalFilename(), + celExpressions, + policySet + ); } catch (IOException e) { return ResponseEntity.internalServerError().build(); } diff --git a/src/main/java/com/metaformsystems/redline/domain/service/Constants.java b/src/main/java/com/metaformsystems/redline/domain/service/Constants.java index 2817b78..e116b4f 100644 --- a/src/main/java/com/metaformsystems/redline/domain/service/Constants.java +++ b/src/main/java/com/metaformsystems/redline/domain/service/Constants.java @@ -9,40 +9,17 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - refactoring * */ package com.metaformsystems.redline.domain.service; -import com.metaformsystems.redline.infrastructure.client.management.dto.Criterion; -import com.metaformsystems.redline.infrastructure.client.management.dto.NewContractDefinition; -import com.metaformsystems.redline.infrastructure.client.management.dto.NewPolicyDefinition; import com.metaformsystems.redline.infrastructure.client.management.dto.PolicySet; -import java.util.List; -import java.util.Set; - public interface Constants { String ASSET_PERMISSION = "membership_asset"; - String MEMBERSHIP_POLICY_ID = "membership_policy"; String MEMBERSHIP_EXPRESSION_ID = "membership_expr"; String MEMBERSHIP_EXPRESSION = "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == 'MembershipCredential')).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))"; - String CONTRACT_DEFINITION_ID = "membership_contract_definition"; - - // all files that are uploaded fall under this policy: the MembershipCredential must be presented to view the EDC asset - NewPolicyDefinition MEMBERSHIP_POLICY = NewPolicyDefinition.Builder.aNewPolicyDefinition() - .id(MEMBERSHIP_POLICY_ID) - .policy(new PolicySet(List.of(new PolicySet.Permission("use", - List.of(new PolicySet.Constraint("MembershipCredential", "eq", "active")))))) - .build(); - - // all new assets must have privateProperties: "permission" - "membership_asset", so that they are affected by this contract def - NewContractDefinition MEMBERSHIP_CONTRACT_DEFINITION = NewContractDefinition.Builder.aNewContractDefinition() - .id(CONTRACT_DEFINITION_ID) - .accessPolicyId(MEMBERSHIP_POLICY_ID) - .contractPolicyId(MEMBERSHIP_POLICY_ID) - .assetsSelector(Set.of(new Criterion("privateProperties.'https://w3id.org/edc/v0.0.1/ns/permission'", "=", ASSET_PERMISSION))) - .build(); - - + PolicySet.Constraint MEMBERSHIP_CONSTRAINT = new PolicySet.Constraint("MembershipCredential", "eq", "active"); } diff --git a/src/main/java/com/metaformsystems/redline/domain/service/DataAccessService.java b/src/main/java/com/metaformsystems/redline/domain/service/DataAccessService.java index 59cafac..cfc6edf 100644 --- a/src/main/java/com/metaformsystems/redline/domain/service/DataAccessService.java +++ b/src/main/java/com/metaformsystems/redline/domain/service/DataAccessService.java @@ -9,12 +9,14 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - CEL and policy creation for uploaded file * */ package com.metaformsystems.redline.domain.service; import com.metaformsystems.redline.api.dto.request.TransferProcessRequest; +import com.metaformsystems.redline.infrastructure.client.management.dto.ContractRequest; import com.metaformsystems.redline.api.dto.response.FileResource; import com.metaformsystems.redline.domain.entity.UploadedFile; import com.metaformsystems.redline.domain.exception.ObjectNotFoundException; @@ -25,7 +27,10 @@ import com.metaformsystems.redline.infrastructure.client.management.dto.Catalog; import com.metaformsystems.redline.infrastructure.client.management.dto.CelExpression; import com.metaformsystems.redline.infrastructure.client.management.dto.ContractNegotiation; -import com.metaformsystems.redline.infrastructure.client.management.dto.ContractRequest; +import com.metaformsystems.redline.infrastructure.client.management.dto.Criterion; +import com.metaformsystems.redline.infrastructure.client.management.dto.NewContractDefinition; +import com.metaformsystems.redline.infrastructure.client.management.dto.NewPolicyDefinition; +import com.metaformsystems.redline.infrastructure.client.management.dto.PolicySet; import com.metaformsystems.redline.infrastructure.client.management.dto.TransferProcess; import com.metaformsystems.redline.infrastructure.client.management.dto.TransferRequest; import org.slf4j.Logger; @@ -39,6 +44,7 @@ import java.io.InputStream; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,10 +55,9 @@ import java.util.stream.Stream; import static com.metaformsystems.redline.domain.service.Constants.ASSET_PERMISSION; -import static com.metaformsystems.redline.domain.service.Constants.MEMBERSHIP_CONTRACT_DEFINITION; +import static com.metaformsystems.redline.domain.service.Constants.MEMBERSHIP_CONSTRAINT; import static com.metaformsystems.redline.domain.service.Constants.MEMBERSHIP_EXPRESSION; import static com.metaformsystems.redline.domain.service.Constants.MEMBERSHIP_EXPRESSION_ID; -import static com.metaformsystems.redline.domain.service.Constants.MEMBERSHIP_POLICY; @Service public class DataAccessService { @@ -72,11 +77,23 @@ public DataAccessService(DataPlaneApiClient dataPlaneApiClient, WebDidResolver w } @Transactional - public void uploadFileForParticipant(Long participantId, Map publicMetadata, Map privateMetadata, InputStream fileStream, String contentType, String originalFilename) { + public void uploadFileForParticipant(Long participantId, Map publicMetadata, Map privateMetadata, InputStream fileStream, String contentType, String originalFilename, List celExpressions, PolicySet policySet) { var participant = participantRepository.findById(participantId).orElseThrow(() -> new ObjectNotFoundException("Participant not found with id: " + participantId)); var participantContextId = participant.getParticipantContextId(); + + //-1. create CEL expressions + if (celExpressions != null) { + celExpressions.forEach(celExpression -> { + try { + managementApiClient.createCelExpression(celExpression); + } catch (WebClientResponseException.Conflict e) { + //do nothing, CEL expression already exists + } + }); + } + //0. upload file to data plane var assetId = UUID.randomUUID().toString(); publicMetadata.put("assetId", assetId); @@ -105,15 +122,27 @@ public void uploadFileForParticipant(Long participantId, Map pub } //2. create policy - var policy = MEMBERSHIP_POLICY; - policy.setId(UUID.randomUUID().toString()); + if (policySet != null) { + var constraints = new ArrayList<>(List.of(MEMBERSHIP_CONSTRAINT)); + constraints.addAll(policySet.getPermission().getFirst().getConstraint()); + policySet.getPermission().getFirst().setConstraint(constraints); + } else { + policySet = new PolicySet(List.of(new PolicySet.Permission("use", + new ArrayList<>(List.of(MEMBERSHIP_CONSTRAINT)) + ))); + } + var policy = NewPolicyDefinition.Builder.aNewPolicyDefinition() + .id(UUID.randomUUID().toString()) + .policy(policySet).build(); managementApiClient.createPolicy(participantContextId, policy); //3. create contract definition if none exists - var contractDef = MEMBERSHIP_CONTRACT_DEFINITION; - contractDef.setId(UUID.randomUUID().toString()); - contractDef.setContractPolicyId(policy.getId()); - contractDef.setAccessPolicyId(policy.getId()); + var contractDef = NewContractDefinition.Builder.aNewContractDefinition() + .id(UUID.randomUUID().toString()) + .contractPolicyId(policy.getId()) + .accessPolicyId(policy.getId()) + .assetsSelector(Set.of(new Criterion("id", "=", assetId))) + .build(); managementApiClient.createContractDefinition(participantContextId, contractDef); diff --git a/src/test/java/com/metaformsystems/redline/TransferEndToEndTest.java b/src/test/java/com/metaformsystems/redline/TransferEndToEndTest.java index 714a345..2c092ae 100644 --- a/src/test/java/com/metaformsystems/redline/TransferEndToEndTest.java +++ b/src/test/java/com/metaformsystems/redline/TransferEndToEndTest.java @@ -9,6 +9,7 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - Add CEL and access constraint * */ @@ -25,7 +26,9 @@ import com.metaformsystems.redline.api.dto.response.Participant; import com.metaformsystems.redline.api.dto.response.Tenant; import com.metaformsystems.redline.infrastructure.client.management.dto.Catalog; +import com.metaformsystems.redline.infrastructure.client.management.dto.CelExpression; import com.metaformsystems.redline.infrastructure.client.management.dto.Constraint; +import com.metaformsystems.redline.infrastructure.client.management.dto.PolicySet; import com.metaformsystems.redline.infrastructure.client.management.dto.TransferProcess; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; @@ -39,6 +42,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import static io.restassured.RestAssured.given; @@ -47,7 +51,7 @@ /** * This test runs through a full participant deployment for consumer and provider plus a data transfer between them. - * For this test a running instance of JAD is required, and the Redline API Server must be reachable at http://redline.localhost. + * For this test a running instance of JAD is required, and the Redline API Server must be reachable at http://redline.localhost:8080. */ @EnabledIfEnvironmentVariable(named = "ENABLE_E2E_TESTS", matches = "true", disabledReason = "This can only run if ENABLE_E2E_TESTS=true is set in the environment.") public class TransferEndToEndTest { @@ -80,11 +84,22 @@ void testTransferFile() throws Exception { log.info("uploading file to provider"); // upload file for consumer - this creates asset, policy, contract-def, etc. + var celExpressions = List.of(CelExpression.Builder.aNewCelExpression() + .id("counter-party-id-" + slug) + .leftOperand("CounterPartyId") + .description("Counter Party Access Policy") + .expression("ctx.agent.id == this.rightOperand") + .scopes(Set.of("catalog", "contract.negotiation", "transfer.process")) + .build()); + var policySet = new PolicySet(List.of(new PolicySet.Permission("use", + List.of(new PolicySet.Constraint("CounterPartyId", "eq", consumerDid))))); baseRequest() .contentType(ContentType.MULTIPART) .multiPart("file", "testfile.txt", "This is a test file.".getBytes()) .multiPart("publicMetadata", "{\"slug\": \"%s\"}".formatted(slug), "application/json") .multiPart("privateMetadata", "{\"privateSlug\": \"%s\"}".formatted(slug), "application/json") + .multiPart("celExpressions", new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(celExpressions), "application/json") + .multiPart("constraints", new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(policySet), "application/json") .post("/api/ui/service-providers/%s/tenants/%s/participants/%s/files".formatted(SERVICE_PROVIDER_ID, provider.tenantId(), provider.participantId())) .then() .statusCode(200); diff --git a/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java b/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java index 89bfdb8..9b73322 100644 --- a/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java +++ b/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java @@ -9,6 +9,7 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - improve tests * */ @@ -168,6 +169,13 @@ void shouldUploadFile() throws Exception { publicMetadata.getHeaders().setContentType(MediaType.APPLICATION_JSON); var privateMetadata = new MockPart("privateMetadata", "{\"private\": \"value\"}".getBytes()); privateMetadata.getHeaders().setContentType(MediaType.APPLICATION_JSON); + var celExpressions = new MockPart("celExpressions", "[{\"id\":\"custom-expression\",\"leftOperand\":\"CustomCredential\",\"description\":\"Custom expression\",\"expression\":\"true\",\"scopes\":[\"catalog\"]}]".getBytes()); + celExpressions.getHeaders().setContentType(MediaType.APPLICATION_JSON); + var constraints = new MockPart("constraints", "[{\"leftOperand\":\"purpose\",\"operator\":\"eq\",\"rightOperand\":\"test\"}]".getBytes()); + constraints.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + // mock create-cel-expression (custom) + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // Mock the upload response from the dataplane mockWebServer.enqueue(new MockResponse() @@ -175,10 +183,10 @@ void shouldUploadFile() throws Exception { .setBody("{\"id\": \"generated-file-id-123\"}") .addHeader("Content-Type", "application/json")); - // mock create-cel-expression + // mock create-asset mockWebServer.enqueue(new MockResponse().setResponseCode(200)); - // mock create-asset + // mock create-cel-expression (membership) mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // mock create-policy @@ -191,7 +199,7 @@ void shouldUploadFile() throws Exception { mockMvc.perform(multipart("/api/ui/service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/files", serviceProvider.getId(), tenant.getId(), participant.getId()) .file(mockFile) - .part(publicMetadata, privateMetadata)) + .part(publicMetadata, privateMetadata, celExpressions, constraints)) .andExpect(status().isOk()); assertThat(participantRepository.findById(participant.getId())).isPresent() @@ -199,7 +207,7 @@ void shouldUploadFile() throws Exception { } @Test - void shouldUploadFile_whenPolicyAndContractDefExist() throws Exception { + void shouldFailUploadFile_whenPolicyAndContractDefExist() throws Exception { // Create a tenant and participant var tenant = new Tenant(); tenant.setName("Test Tenant"); @@ -225,31 +233,30 @@ void shouldUploadFile_whenPolicyAndContractDefExist() throws Exception { ); // Create metadata - var metadataPart = new MockPart("metadata", "{\"foo\": \"bar\"}".getBytes()); - metadataPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); + var publicMetadata = new MockPart("publicMetadata", "{\"foo\": \"bar\"}".getBytes()); + publicMetadata.getHeaders().setContentType(MediaType.APPLICATION_JSON); + var privateMetadata = new MockPart("privateMetadata", "{\"private\": \"value\"}".getBytes()); + privateMetadata.getHeaders().setContentType(MediaType.APPLICATION_JSON); - // mock create-cel-expression - mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + // Mock the upload response from the dataplane + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"id\": \"generated-file-id-123\"}") + .addHeader("Content-Type", "application/json")); // mock create-asset mockWebServer.enqueue(new MockResponse().setResponseCode(200)); - // mock create-policy - mockWebServer.enqueue(new MockResponse().setResponseCode(409)); + // mock create-cel-expression (membership) + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); - //mock create-contractdef + // mock create-policy mockWebServer.enqueue(new MockResponse().setResponseCode(409)); - // Mock the upload response from the dataplane - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .setBody("{\"id\": \"generated-file-id-123\"}") - .addHeader("Content-Type", "application/json")); - mockMvc.perform(multipart("/api/ui/service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/files", serviceProvider.getId(), tenant.getId(), participant.getId()) .file(mockFile) - .part(metadataPart)) + .part(publicMetadata, privateMetadata)) .andExpect(status().isInternalServerError()); } @@ -367,4 +374,4 @@ void shouldRequestContractWithConstraints() throws Exception { .content(objectMapper.writeValueAsString(contractRequest))) .andExpect(status().isOk()); } -} \ No newline at end of file +} diff --git a/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java b/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java index 076cef7..713c83a 100644 --- a/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java @@ -9,6 +9,7 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - CEL and Constraint * */ @@ -27,8 +28,10 @@ import com.metaformsystems.redline.domain.repository.TenantRepository; import com.metaformsystems.redline.infrastructure.client.management.dto.Constraint; import com.metaformsystems.redline.infrastructure.client.management.dto.ContractRequest; +import com.metaformsystems.redline.infrastructure.client.management.dto.CelExpression; import com.metaformsystems.redline.infrastructure.client.management.dto.Obligation; import com.metaformsystems.redline.infrastructure.client.management.dto.Offer; +import com.metaformsystems.redline.infrastructure.client.management.dto.PolicySet; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -48,6 +51,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; @@ -91,6 +95,8 @@ static void configureProperties(DynamicPropertyRegistry registry) { registry.add("tenant-manager.url", () -> "http://%s:%s/tm".formatted(mockBackEndHost, mockBackEndPort)); registry.add("vault.url", () -> "http://%s:%s/vault".formatted(mockBackEndHost, mockBackEndPort)); registry.add("controlplane.url", () -> "http://%s:%s/cp".formatted(mockBackEndHost, mockBackEndPort)); + registry.add("dataplane.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); + registry.add("dataplane.internal.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); } @AfterEach @@ -153,6 +159,57 @@ void shouldRequestCatalog_andBypassCacheWithNoCache() { assertThat(mockWebServer.getRequestCount()).isEqualTo(2); } + @Test + void shouldUploadFileWithCelExpressionsAndConstraints() { + var participant = createAndSaveParticipant("ctx-upload-1", "did:web:me"); + + // custom CEL expression + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // dataplane upload response + mockWebServer.enqueue(new MockResponse() + .setBody("{\"id\": \"generated-file-id-123\"}") + .addHeader("Content-Type", "application/json")); + + // asset creation + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // membership CEL expression + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // policy creation + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // contract definition + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + var celExpressions = List.of(CelExpression.Builder.aNewCelExpression() + .id("custom-expression") + .leftOperand("CustomCredential") + .description("Custom expression") + .expression("true") + .scopes(Set.of("catalog")) + .build()); + + var policySet = new PolicySet(List.of(new PolicySet.Permission("use", + List.of(new PolicySet.Constraint("purpose", "eq", "test"))))); + + dataAccessService.uploadFileForParticipant( + participant.getId(), + new java.util.HashMap<>(Map.of("foo", "bar")), + new java.util.HashMap<>(Map.of("private", "value")), + new java.io.ByteArrayInputStream("file-data".getBytes()), + "text/plain", + "file.txt", + celExpressions, + policySet + ); + + assertThat(participantRepository.findById(participant.getId())) + .isPresent() + .hasValueSatisfying(p -> assertThat(p.getUploadedFiles()).hasSize(1)); + } + @Test void shouldRequestCatalog_andRefreshWhenMaxAgeIsZero() { var participant = createAndSaveParticipant("ctx-3", "did:web:me");