Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
*/

Expand All @@ -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;
Expand Down Expand Up @@ -82,14 +85,22 @@ public EdcDataController(DataAccessService dataAccessService, ObjectMapper objec
public ResponseEntity<Void> uploadFile(@PathVariable Long participantId,
@PathVariable Long tenantId,
@PathVariable Long providerId,
@RequestPart("publicMetadata") String publicMetadata,
@RequestPart("privateMetadata") String privateMetadata,
@RequestPart("publicMetadata") Map<String, Object> publicMetadata,
@RequestPart("privateMetadata") Map<String, Object> privateMetadata,
@RequestPart(value = "celExpressions", required = false) List<CelExpression> celExpressions,
@RequestPart(value = "policySet", required = false) PolicySet policySet,
@RequestPart("file") MultipartFile file) {

try {
var publicMetadataMap = objectMapper.readValue(publicMetadata, new TypeReference<Map<String, Object>>() {});
var privateMetadataMap = objectMapper.readValue(privateMetadata, new TypeReference<Map<String, Object>>() {});
dataAccessService.uploadFileForParticipant(participantId, publicMetadataMap, privateMetadataMap, file.getInputStream(), file.getContentType(), file.getOriginalFilename());
dataAccessService.uploadFileForParticipant(
participantId,
publicMetadata,
privateMetadata,
file.getInputStream(),
file.getContentType(),
file.getOriginalFilename(),
celExpressions != null ? celExpressions : List.of(),
policySet
);
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* 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
*
*/

Expand All @@ -26,6 +27,10 @@
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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -72,7 +77,7 @@ public DataAccessService(DataPlaneApiClient dataPlaneApiClient, WebDidResolver w
}

@Transactional
public void uploadFileForParticipant(Long participantId, Map<String, Object> publicMetadata, Map<String, Object> privateMetadata, InputStream fileStream, String contentType, String originalFilename) {
public void uploadFileForParticipant(Long participantId, Map<String, Object> publicMetadata, Map<String, Object> privateMetadata, InputStream fileStream, String contentType, String originalFilename, List<CelExpression> celExpressions, PolicySet policySet) {

var participant = participantRepository.findById(participantId).orElseThrow(() -> new ObjectNotFoundException("Participant not found with id: " + participantId));
var participantContextId = participant.getParticipantContextId();
Expand All @@ -85,39 +90,54 @@ public void uploadFileForParticipant(Long participantId, Map<String, Object> pub
var response = dataPlaneApiClient.uploadMultipart(participantContextId, combinedMetadata, fileStream);
var fileId = response.id();

//1. create asset
publicMetadata.put("fileId", fileId);
//1. create CEL expressions
var expressions = new ArrayList<>(celExpressions);
expressions.add(CelExpression.Builder.aNewCelExpression()
.id(MEMBERSHIP_EXPRESSION_ID)
.leftOperand("MembershipCredential")
.description("Expression for evaluating membership credential")
.scopes(Set.of("catalog", "contract.negotiation", "transfer.process"))
.expression(MEMBERSHIP_EXPRESSION)
.build());
expressions.forEach(celExpression -> {
try {
managementApiClient.createCelExpression(celExpression);
} catch (WebClientResponseException.Conflict e) {
//do nothing, CEL expression already exists
}
});

//2. create asset
publicMetadata.put("fileId", fileId);
var asset = createAsset(assetId, publicMetadata, privateMetadata, contentType, originalFilename);
managementApiClient.createAsset(participantContextId, asset);

// create CEL expression
try {
managementApiClient.createCelExpression(CelExpression.Builder.aNewCelExpression()
.id(MEMBERSHIP_EXPRESSION_ID)
.leftOperand("MembershipCredential")
.description("Expression for evaluating membership credential")
.scopes(Set.of("catalog", "contract.negotiation", "transfer.process"))
.expression(MEMBERSHIP_EXPRESSION)
.build());
} catch (WebClientResponseException.Conflict e) {
//do nothing, CEL expression already exists
//3. create policy
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))
)));
}

//2. create policy
var policy = MEMBERSHIP_POLICY;
policy.setId(UUID.randomUUID().toString());
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());
//4. create contract definition if none exists
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);


//2. track uploaded file in DB
//5. track uploaded file in DB
participant.getUploadedFiles().add(new UploadedFile(fileId, originalFilename, contentType, combinedMetadata));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
*/

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading