diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index a0f4c65..714eccf 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -87,7 +87,7 @@ jobs: - name: "Deploy JAD applications" run: |- # this is crucial - without it, KinD would take the prebuilt images from GHCR - grep -rlZ "imagePullPolicy:.*Always" . | xargs sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" + grep -rlZ "imagePullPolicy:.*Always" . | xargs --null sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" kubectl apply -f k8s/apps kubectl wait --namespace edc-v \ diff --git a/README.md b/README.md index 15ea5f2..b8cf426 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ following the following steps: ```shell kind load docker-image \ - ghcr.io/metaform/jad/controlplane:0.16.0-SNAPSHOT \ - ghcr.io/metaform/jad/identity-hub:0.16.0-SNAPSHOT \ - ghcr.io/metaform/jad/issuerservice:0.16.0-SNAPSHOT \ - ghcr.io/metaform/jad/dataplane:0.16.0-SNAPSHOT \ + ghcr.io/metaform/jad/controlplane:latest \ + ghcr.io/metaform/jad/identity-hub:latest \ + ghcr.io/metaform/jad/issuerservice:latest \ + ghcr.io/metaform/jad/dataplane:latest \ ghcr.io/metaform/jad/postgres:wal2json -n edcv ``` or if you're a bash god: @@ -134,6 +134,26 @@ This sets up accounts in the IssuerService, the IdentityHub and the ControlPlane `MembershipCredential` to each new participant. It also seeds dummy data to each participant, specifically an Asset, a Policy and a ContractDefinition. +## Seeding EDC-V CEL Expressions + +For evaluating policies EDC-V makes usage of the CEL (Common Expression Language) engine. To demonstrate this, we +will create a simple CEL expression that allows data access only to participants that possess a valid Membership +Credential. + +Run the requests in the `Create CEL expression` request in folder `EDC-V Management` in the same Bruno collection +to create the CEL expression in the ControlPlane. + +![img.png](docs/images/bruno_cel_expr.png) + +## Seeding the Provider + +Before we can transfer data, we need to seed the Provider with an asset, a policy and a contract definition. This is +done by running the requests in the `EDC-V Management (Provider)` folder in the same Bruno collection. Again, make sure +to select the +`"KinD Local"` environment. + +![img.png](docs/images/bruno_provider_seed.png) + ## Transfer Data EDC-V offers a one-stop-shop API to transfer data. This is achieved by two endpoints, one that fetches the catalog ( @@ -225,22 +245,22 @@ To start, edit the `readinessProbe` section of the `keycloak` deployment manifes ```yaml # keycloak.yaml, Line 79ff - readinessProbe: - httpGet: - path: /health/ready - port: 9000 - initialDelaySeconds: 30 # changed - periodSeconds: 10 # changed - successThreshold: 1 - failureThreshold: 15 # changed - livenessProbe: - httpGet: - path: /health/live - port: 9000 - initialDelaySeconds: 30 # changed - periodSeconds: 10 # changed - successThreshold: 1 - failureThreshold: 15 # changed +readinessProbe: + httpGet: + path: /health/ready + port: 9000 + initialDelaySeconds: 30 # changed + periodSeconds: 10 # changed + successThreshold: 1 + failureThreshold: 15 # changed +livenessProbe: + httpGet: + path: /health/live + port: 9000 + initialDelaySeconds: 30 # changed + periodSeconds: 10 # changed + successThreshold: 1 + failureThreshold: 15 # changed ``` diff --git a/docs/images/bruno_cel_expr.png b/docs/images/bruno_cel_expr.png new file mode 100644 index 0000000..dabfa9b Binary files /dev/null and b/docs/images/bruno_cel_expr.png differ diff --git a/docs/images/bruno_provider_seed.png b/docs/images/bruno_provider_seed.png new file mode 100644 index 0000000..287d3ac Binary files /dev/null and b/docs/images/bruno_provider_seed.png differ diff --git a/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/ApiExtension.java b/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/ApiExtension.java index 7e159f9..0a0003e 100644 --- a/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/ApiExtension.java +++ b/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/ApiExtension.java @@ -14,11 +14,8 @@ package org.eclipse.edc.virtualized; -import org.eclipse.edc.connector.controlplane.services.spi.asset.AssetService; import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogService; -import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; -import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessService; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.edr.spi.store.EndpointDataReferenceStore; @@ -65,12 +62,6 @@ public class ApiExtension implements ServiceExtension { @Inject private DataPlaneSelectorService selectorService; @Inject - private AssetService assetService; - @Inject - private PolicyDefinitionService policyService; - @Inject - private ContractDefinitionService contractDefinitionService; - @Inject private TransactionContext transactionContext; @Inject private ContractNegotiationService contractNegotiationService; @@ -83,7 +74,7 @@ public class ApiExtension implements ServiceExtension { @Override public void initialize(ServiceExtensionContext context) { - var onboardingService = new OnboardingService(transactionContext, service, configService, vault, selectorService, assetService, policyService, contractDefinitionService, url); + var onboardingService = new OnboardingService(transactionContext, service, configService, vault, selectorService, url); webService.registerResource(ApiContext.MANAGEMENT, new ParticipantContextApiController(onboardingService)); var dataRequestService = new DataRequestService(contractNegotiationService, transferProcessService, didResolverRegistry, edrStore); webService.registerResource(ApiContext.MANAGEMENT, new DataApiController(catalogService, didResolverRegistry, participantContextService, dataRequestService)); diff --git a/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/service/OnboardingService.java b/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/service/OnboardingService.java index d9ca042..a298e31 100644 --- a/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/service/OnboardingService.java +++ b/extensions/api/mgmt/src/main/java/org/eclipse/edc/virtualized/service/OnboardingService.java @@ -16,12 +16,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; -import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; -import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; -import org.eclipse.edc.connector.controlplane.services.spi.asset.AssetService; -import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; -import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; import org.eclipse.edc.participantcontext.spi.config.model.ParticipantContextConfiguration; @@ -30,17 +24,12 @@ import org.eclipse.edc.participantcontext.spi.types.ParticipantContext; import org.eclipse.edc.participantcontext.spi.types.ParticipantContextState; import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.result.ServiceFailure; -import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.security.Vault; -import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.transaction.spi.TransactionContext; import org.eclipse.edc.virtualized.api.management.ParticipantManifest; -import java.util.List; import java.util.Map; -import java.util.UUID; /** * This service is a quick-n-dirty onboarding agent, that performs all necessary tasks required to onboard a new participant into the control plane: @@ -68,9 +57,6 @@ public class OnboardingService { private final ParticipantContextConfigService configService; private final Vault vault; private final DataPlaneSelectorService dataPlaneSelectorService; - private final AssetService assetService; - private final PolicyDefinitionService policyService; - private final ContractDefinitionService contractDefinitionService; private final String defaultVaultUrl; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -78,18 +64,12 @@ public OnboardingService(TransactionContext transactionContext, ParticipantConte ParticipantContextConfigService configService, Vault vault, DataPlaneSelectorService dataPlaneSelectorService, - AssetService assetService, - PolicyDefinitionService policyService, - ContractDefinitionService contractDefinitionService, String defaultVaultUrl) { this.transactionContext = transactionContext; this.participantContextStore = participantContextStore; this.configService = configService; this.vault = vault; this.dataPlaneSelectorService = dataPlaneSelectorService; - this.assetService = assetService; - this.policyService = policyService; - this.contractDefinitionService = contractDefinitionService; this.defaultVaultUrl = defaultVaultUrl; } @@ -134,12 +114,6 @@ public void onboardParticipant(ParticipantManifest manifest) { .build()) .orElseThrow(OnboardingException::new); - - var assetId = UUID.randomUUID().toString(); - createAssets(assetId, participantContextId) - .compose(a -> createPolicies(participantContextId)) - .compose(p -> createContractDefinitions(assetId, p.getId(), participantContextId)) - .orElseThrow(OnboardingException::new); }); } @@ -151,41 +125,4 @@ private String toJson(Object obj) { throw new EdcException(e); } } - - private ServiceResult createPolicies(String participantContextId) { - var policy = PolicyDefinition.Builder.newInstance() - .id(UUID.randomUUID().toString()) - .participantContextId(participantContextId) - .policy(Data.MEMBERSHIP_POLICY) - .build(); - - return policyService.create(policy); - } - - private ServiceResult createContractDefinitions(String assetId, String policyId, String participantContextId) { - - var contractDefinition = ContractDefinition.Builder.newInstance() - .id(UUID.randomUUID().toString()) - .participantContextId(participantContextId) - .contractPolicyId(policyId) - .accessPolicyId(policyId) - .assetsSelector(List.of(new Criterion("https://w3id.org/edc/v0.0.1/ns/id", "=", assetId))) - .build(); - return contractDefinitionService.create(contractDefinition); - } - - private ServiceResult createAssets(String assetId, String participantContextId) { - var asset1 = Asset.Builder.newInstance() - .id(assetId) - .participantContextId(participantContextId) - .property("description", "This asset requires the Membership credential to access") - .dataAddress(DataAddress.Builder.newInstance() - .type("HttpData") - .property("https://w3id.org/edc/v0.0.1/ns/baseUrl", "https://jsonplaceholder.typicode.com/todos") - .property("https://w3id.org/edc/v0.0.1/ns/proxyPath", "true") - .property("https://w3id.org/edc/v0.0.1/ns/proxyQueryParams", "true") - .build()) - .build(); - return assetService.create(asset1); - } } diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java deleted file mode 100644 index 58ed884..0000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2025 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -import org.eclipse.edc.participant.spi.ParticipantAgentPolicyContext; -import org.eclipse.edc.policy.engine.spi.AtomicConstraintRuleFunction; -import org.eclipse.edc.policy.model.Operator; -import org.eclipse.edc.policy.model.Permission; - -import java.time.Instant; -import java.util.Map; - -public class MembershipCredentialEvaluationFunction extends AbstractCredentialEvaluationFunction implements AtomicConstraintRuleFunction { - public static final String MEMBERSHIP_CONSTRAINT_KEY = "MembershipCredential"; - - private static final String MEMBERSHIP_CLAIM = "membership"; - private static final String SINCE_CLAIM = "since"; - private static final String ACTIVE = "active"; - - private MembershipCredentialEvaluationFunction() { - } - - public static MembershipCredentialEvaluationFunction create() { - return new MembershipCredentialEvaluationFunction<>() { - }; - } - - @SuppressWarnings("unchecked") - @Override - public boolean evaluate(Operator operator, Object rightOperand, Permission permission, C policyContext) { - if (!operator.equals(Operator.EQ)) { - policyContext.reportProblem("Invalid operator '%s', only accepts '%s'".formatted(operator, Operator.EQ)); - return false; - } - if (!ACTIVE.equals(rightOperand)) { - policyContext.reportProblem("Right-operand must be equal to '%s', but was '%s'".formatted(ACTIVE, rightOperand)); - return false; - } - - var pa = policyContext.participantAgent(); - if (pa == null) { - policyContext.reportProblem("No ParticipantAgent found on context."); - return false; - } - var credentialResult = getCredentialList(pa); - if (credentialResult.failed()) { - policyContext.reportProblem(credentialResult.getFailureDetail()); - return false; - } - - return credentialResult.getContent() - .stream() - .filter(vc -> vc.getType().stream().anyMatch(t -> t.endsWith(MEMBERSHIP_CONSTRAINT_KEY))) - .flatMap(vc -> vc.getCredentialSubject().stream().filter(cs -> cs.getClaims().containsKey(MEMBERSHIP_CLAIM))) - .anyMatch(credential -> { - var membershipClaim = (Map) credential.getClaim(MVD_NAMESPACE, MEMBERSHIP_CLAIM); - var membershipStartDate = Instant.parse(membershipClaim.get(SINCE_CLAIM).toString()); - return membershipStartDate.isBefore(Instant.now()); - }); - } - -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java index 3475ca4..acceb9b 100644 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java +++ b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java @@ -22,12 +22,10 @@ import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; import org.eclipse.edc.policy.model.Duty; -import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; -import static org.eclipse.edc.demo.dcp.policy.MembershipCredentialEvaluationFunction.MEMBERSHIP_CONSTRAINT_KEY; import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; public class PolicyEvaluationExtension implements ServiceExtension { @@ -40,13 +38,7 @@ public class PolicyEvaluationExtension implements ServiceExtension { @Override public void initialize(ServiceExtensionContext context) { - - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), TransferProcessPolicyContext.class, TransferProcessPolicyContext.TRANSFER_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), ContractNegotiationPolicyContext.class, ContractNegotiationPolicyContext.NEGOTIATION_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), CatalogPolicyContext.class, CatalogPolicyContext.CATALOG_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - registerDataAccessLevelFunction(); - } private void registerDataAccessLevelFunction() { @@ -57,14 +49,6 @@ private void registerDataAccessLevelFunction() { bindDutyFunction(DataAccessLevelFunction.create(), CatalogPolicyContext.class, CatalogPolicyContext.CATALOG_SCOPE, accessLevelKey); } - private void bindPermissionFunction(AtomicConstraintRuleFunction function, Class contextClass, String scope, String constraintType) { - ruleBindingRegistry.bind("use", scope); - ruleBindingRegistry.bind(ODRL_SCHEMA + "use", scope); - ruleBindingRegistry.bind(constraintType, scope); - - policyEngine.registerFunction(contextClass, Permission.class, constraintType, function); - } - private void bindDutyFunction(AtomicConstraintRuleFunction function, Class contextClass, String scope, String constraintType) { ruleBindingRegistry.bind("use", scope); ruleBindingRegistry.bind(ODRL_SCHEMA + "use", scope); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64d18a5..5e041f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,7 @@ edcv-nats-subscriber-cn = { module = "org.eclipse.edc.virtualized:negotiation-su edcv-nats-publisher-tp = { module = "org.eclipse.edc.virtualized:transfer-process-cdc-publisher-nats", version.ref = "edc" } edcv-nats-publisher-cn = { module = "org.eclipse.edc.virtualized:negotiation-cdc-publisher-nats", version.ref = "edc" } edcv-cel-extension = { module = "org.eclipse.edc.virtualized:cel-extension", version.ref = "edc" } +edcv-cel-store-sql = { module = "org.eclipse.edc.virtualized:cel-store-sql", version.ref = "edc" } # EDC spi dependencies edc-spi-participantcontext = { module = "org.eclipse.edc:connector-participant-context-spi", version.ref = "edc" } diff --git a/k8s/apps/controlplane.yaml b/k8s/apps/controlplane.yaml index 7fdcf94..98b2115 100644 --- a/k8s/apps/controlplane.yaml +++ b/k8s/apps/controlplane.yaml @@ -34,7 +34,7 @@ spec: spec: containers: - name: controlplane - image: ghcr.io/metaform/jad/controlplane:0.16.0-SNAPSHOT + image: ghcr.io/metaform/jad/controlplane:latest imagePullPolicy: Always envFrom: - configMapRef: { name: controlplane-config } diff --git a/k8s/apps/dataplane.yaml b/k8s/apps/dataplane.yaml index bb0b4ba..b651fd9 100644 --- a/k8s/apps/dataplane.yaml +++ b/k8s/apps/dataplane.yaml @@ -34,7 +34,7 @@ spec: spec: containers: - name: dataplane - image: ghcr.io/metaform/jad/dataplane:0.16.0-SNAPSHOT + image: ghcr.io/metaform/jad/dataplane:latest imagePullPolicy: Always envFrom: - configMapRef: diff --git a/k8s/apps/identityhub.yaml b/k8s/apps/identityhub.yaml index d950968..03804d6 100644 --- a/k8s/apps/identityhub.yaml +++ b/k8s/apps/identityhub.yaml @@ -34,7 +34,7 @@ spec: spec: containers: - name: identityhub - image: ghcr.io/metaform/jad/identity-hub:0.16.0-SNAPSHOT + image: ghcr.io/metaform/jad/identity-hub:latest imagePullPolicy: Always envFrom: - configMapRef: diff --git a/k8s/apps/issuerservice.yaml b/k8s/apps/issuerservice.yaml index cb3ed7a..6c2b8a9 100644 --- a/k8s/apps/issuerservice.yaml +++ b/k8s/apps/issuerservice.yaml @@ -34,7 +34,7 @@ spec: spec: containers: - name: issuerservice - image: ghcr.io/metaform/jad/issuerservice:0.16.0-SNAPSHOT + image: ghcr.io/metaform/jad/issuerservice:latest imagePullPolicy: Always ports: - containerPort: 80 diff --git a/launchers/controlplane/build.gradle.kts b/launchers/controlplane/build.gradle.kts index dce1da1..c1fe5ad 100644 --- a/launchers/controlplane/build.gradle.kts +++ b/launchers/controlplane/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { runtimeOnly(libs.edcv.nats.subscriber.tp) runtimeOnly(libs.edcv.nats.subscriber.cn) runtimeOnly(libs.edcv.cel.extension) + runtimeOnly(libs.edcv.cel.store.sql) runtimeOnly(libs.edc.core.connector) runtimeOnly(libs.edc.core.runtime) diff --git a/requests/EDC-V Onboarding/Data Transfer/folder.bru b/requests/EDC-V Onboarding/Data Transfer/folder.bru index 6553776..a046881 100644 --- a/requests/EDC-V Onboarding/Data Transfer/folder.bru +++ b/requests/EDC-V Onboarding/Data Transfer/folder.bru @@ -1,6 +1,6 @@ meta { name: Data Transfer - seq: 5 + seq: 7 } auth { diff --git a/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Asset.bru b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Asset.bru new file mode 100644 index 0000000..010acf2 --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Asset.bru @@ -0,0 +1,51 @@ +meta { + name: Create Asset + type: http + seq: 1 +} + +post { + url: {{baseURL}}/cp/api/mgmt/v4alpha/participants/provider/assets + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provider + client_secret: provider-secret + scope: management-api:write + credentials_placement: basic_auth_header + credentials_id: credentials + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "Asset", + "@id": "{{ASSET_ID}}", + "properties": { + "description": "This asset requires the Membership credential to access" + }, + "dataAddress": { + "@type": "DataAddress", + "type": "HttpData", + "baseUrl": "https://jsonplaceholder.typicode.com/todos", + "proxyPath": "true", + "proxyQueryParams": "true" + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Contract Definition.bru b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Contract Definition.bru new file mode 100644 index 0000000..eced3e3 --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Contract Definition.bru @@ -0,0 +1,51 @@ +meta { + name: Create Contract Definition + type: http + seq: 3 +} + +post { + url: {{baseURL}}/cp/api/mgmt/v4alpha/participants/provider/contractdefinitions + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provider + client_secret: provider-secret + scope: management-api:write + credentials_placement: basic_auth_header + credentials_id: credentials + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "ContractDefinition", + "@id": "{{CONTRACT_DEF_ID}}", + "accessPolicyId": "{{POLICY_DEF_ID}}", + "contractPolicyId": "{{POLICY_DEF_ID}}", + "assetsSelector": [ + { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator" : "=", + "operandRight" : "{{ASSET_ID}}" + } + ] + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Policy.bru b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Policy.bru new file mode 100644 index 0000000..fced2fd --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management (Provider)/Create Policy.bru @@ -0,0 +1,56 @@ +meta { + name: Create Policy + type: http + seq: 2 +} + +post { + url: {{baseURL}}/cp/api/mgmt/v4alpha/participants/provider/policydefinitions + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provider + client_secret: provider-secret + scope: management-api:write + credentials_placement: basic_auth_header + credentials_id: credentials + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "PolicyDefinition", + "@id": "{{POLICY_DEF_ID}}", + "policy": { + "@type": "Set", + "permission": [ + { + "action": "use", + "constraint" : [ + { + "leftOperand": "MembershipCredential", + "operator": "eq", + "rightOperand": "active" + } + ] + } + ] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management (Provider)/folder.bru b/requests/EDC-V Onboarding/EDC-V Management (Provider)/folder.bru new file mode 100644 index 0000000..0bcc7b5 --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management (Provider)/folder.bru @@ -0,0 +1,14 @@ +meta { + name: EDC-V Management (Provider) + seq: 6 +} + +auth { + mode: inherit +} + +vars:pre-request { + ASSET_ID: membership_asset + POLICY_DEF_ID: membership_policy + CONTRACT_DEF_ID: membership_contract_def +} diff --git a/requests/EDC-V Onboarding/EDC-V Management/Create CEL expression.bru b/requests/EDC-V Onboarding/EDC-V Management/Create CEL expression.bru new file mode 100644 index 0000000..72942dc --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management/Create CEL expression.bru @@ -0,0 +1,45 @@ +meta { + name: Create CEL expression + type: http + seq: 2 +} + +post { + url: {{baseURL}}/cp/api/mgmt/v4alpha/celexpressions + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provisioner + client_secret: provisioner-secret + scope: + credentials_placement: basic_auth_header + credentials_id: edcv-provisioner + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2", + "https://w3id.org/edc/virtual-connector/management/v2" + ], + "@type": "CelExpression", + "@id": "{{CEL_EXPR_ID}}", + "leftOperand": "MembershipCredential", + "description": "Expression for evaluating membership credential", + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == 'MembershipCredential')).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management/Get CEL expression.bru b/requests/EDC-V Onboarding/EDC-V Management/Get CEL expression.bru new file mode 100644 index 0000000..d4fafad --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management/Get CEL expression.bru @@ -0,0 +1,31 @@ +meta { + name: Get CEL expression + type: http + seq: 3 +} + +get { + url: {{baseURL}}/cp/api/mgmt/v4alpha/celexpressions/{{CEL_EXPR_ID}} + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provisioner + client_secret: provisioner-secret + scope: + credentials_placement: basic_auth_header + credentials_id: edcv-provisioner + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management/Update CEL expression.bru b/requests/EDC-V Onboarding/EDC-V Management/Update CEL expression.bru new file mode 100644 index 0000000..4252b79 --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management/Update CEL expression.bru @@ -0,0 +1,45 @@ +meta { + name: Update CEL expression + type: http + seq: 2 +} + +put { + url: {{baseURL}}/cp/api/mgmt/v4alpha/celexpressions/{{CEL_EXPR_ID}} + body: json + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: {{KC_HOST}}/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: provisioner + client_secret: provisioner-secret + scope: + credentials_placement: basic_auth_header + credentials_id: edcv-provisioner + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2", + "https://w3id.org/edc/virtual-connector/management/v2" + ], + "@type": "CelExpression", + "@id": "{{CEL_EXPR_ID}}", + "leftOperand": "MembershipCredential", + "description": "Expression for evaluating membership credential", + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == 'MembershipCredential')).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management/folder.bru b/requests/EDC-V Onboarding/EDC-V Management/folder.bru new file mode 100644 index 0000000..d71167d --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management/folder.bru @@ -0,0 +1,12 @@ +meta { + name: EDC-V Management + seq: 5 +} + +auth { + mode: inherit +} + +vars:pre-request { + CEL_EXPR_ID: membership_expr +} diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java index 6a4f5b3..8e840b3 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java @@ -30,6 +30,7 @@ import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakToken; import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakUser; import static org.eclipse.edc.jad.tests.KeycloakApi.getAccessToken; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; /** * This test class executes a series of REST requests against several components to verify that an end-to-end @@ -67,6 +68,7 @@ void testDataTransfer() { var provisionerToken = createKeycloakToken("provisioner", "provisioner-secret", "issuer-admin-api:write", "identity-api:write", "management-api:write", "identity-api:read"); createIssuerTenant(provisionerToken); + createCelExpression(provisionerToken); var participantIdBase64 = Base64.getEncoder().encodeToString(ISSUER_CLIENT_ID.getBytes()); monitor.withPrefix("Issuer").info("Creating attestation and credential definitions"); @@ -84,6 +86,14 @@ void testDataTransfer() { var providerPo = new ParticipantOnboarding("provider", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider", ISSUER_CLIENT_ID, provisionerToken, monitor.withPrefix("Provider")); providerPo.execute(credentialDefId); + // seed provider + monitor.info("Seeding provider"); + var providerAccesstoken = getAccessToken("provider", "provider-secret", "management-api:write"); + + var assetId = createAsset(providerAccesstoken.accessToken()); + var policyDefId = createPolicyDef(providerAccesstoken.accessToken()); + createContractDef(providerAccesstoken.accessToken(), policyDefId, assetId); + // perform data transfer monitor.info("Starting data transfer"); var accessToken = getAccessToken("consumer", "consumer-secret", "management-api:read"); @@ -181,4 +191,60 @@ private CreateParticipantContextResponse createIssuerTenant(String accessToken) .extract() .body().as(CreateParticipantContextResponse.class); } + + private void createCelExpression(String accessToken) { + var template = loadResourceFile("create_cel_expression.json"); + + given() + .baseUri(BASE_URL) + .auth().oauth2(accessToken) + .contentType("application/json") + .body(template) + .post("/cp/api/mgmt/v4alpha/celexpressions") + .then() + .statusCode(200); + } + + private String createAsset(String accessToken) { + var template = loadResourceFile("asset.json"); + return given() + .baseUri(BASE_URL) + .auth().oauth2(accessToken) + .contentType("application/json") + .body(template) + .post("/cp/api/mgmt/v4alpha/participants/provider/assets") + .then() + .statusCode(200) + .extract().jsonPath().getString(ID); + } + + private String createPolicyDef(String accessToken) { + var template = loadResourceFile("policy-def.json"); + return given() + .baseUri(BASE_URL) + .auth().oauth2(accessToken) + .contentType("application/json") + .body(template) + .post("/cp/api/mgmt/v4alpha/participants/provider/policydefinitions") + .then() + .statusCode(200) + .extract().jsonPath().getString(ID); + } + + private String createContractDef(String accessToken, String policyDefId, String assetId) { + var template = loadResourceFile("contract-def.json"); + + template = template.replace("{{policy_def_id}}", policyDefId); + template = template.replace("{{asset_id}}", assetId); + + return given() + .baseUri(BASE_URL) + .auth().oauth2(accessToken) + .contentType("application/json") + .body(template) + .post("/cp/api/mgmt/v4alpha/participants/provider/contractdefinitions") + .then() + .statusCode(200) + .extract().jsonPath().getString(ID); + } } diff --git a/tests/end2end/src/test/resources/asset.json b/tests/end2end/src/test/resources/asset.json new file mode 100644 index 0000000..2934f3a --- /dev/null +++ b/tests/end2end/src/test/resources/asset.json @@ -0,0 +1,16 @@ +{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "Asset", + "properties": { + "description": "This asset requires the Membership credential to access" + }, + "dataAddress": { + "@type": "DataAddress", + "type": "HttpData", + "baseUrl": "https://jsonplaceholder.typicode.com/todos", + "proxyPath": "true", + "proxyQueryParams": "true" + } +} \ No newline at end of file diff --git a/tests/end2end/src/test/resources/contract-def.json b/tests/end2end/src/test/resources/contract-def.json new file mode 100644 index 0000000..89ef757 --- /dev/null +++ b/tests/end2end/src/test/resources/contract-def.json @@ -0,0 +1,16 @@ +{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "ContractDefinition", + "accessPolicyId": "{{policy_def_id}}", + "contractPolicyId": "{{policy_def_id}}", + "assetsSelector": [ + { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator": "=", + "operandRight": "{{asset_id}}" + } + ] +} \ No newline at end of file diff --git a/tests/end2end/src/test/resources/create_cel_expression.json b/tests/end2end/src/test/resources/create_cel_expression.json new file mode 100644 index 0000000..20629de --- /dev/null +++ b/tests/end2end/src/test/resources/create_cel_expression.json @@ -0,0 +1,10 @@ +{ + "@context": [ + "https://w3id.org/edc/connector/management/v2", + "https://w3id.org/edc/virtual-connector/management/v2" + ], + "@type": "CelExpression", + "leftOperand": "MembershipCredential", + "description": "Expression for evaluating membership credential", + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == 'MembershipCredential')).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))" +} \ No newline at end of file diff --git a/tests/end2end/src/test/resources/policy-def.json b/tests/end2end/src/test/resources/policy-def.json new file mode 100644 index 0000000..c46bbeb --- /dev/null +++ b/tests/end2end/src/test/resources/policy-def.json @@ -0,0 +1,21 @@ +{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "PolicyDefinition", + "policy": { + "@type": "Set", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "MembershipCredential", + "operator": "eq", + "rightOperand": "active" + } + ] + } + ] + } +} \ No newline at end of file