diff --git a/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-hashicorp-vault/build.gradle.kts b/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-hashicorp-vault/build.gradle.kts index cec4ef5eb9..ec87b2d36a 100644 --- a/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-hashicorp-vault/build.gradle.kts +++ b/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-hashicorp-vault/build.gradle.kts @@ -37,8 +37,8 @@ dependencies { implementation("org.eclipse.tractusx.edc:agreements:$txVersion") implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion") implementation(project(":edc-extensions:agreements:retirement-evaluation-bootstrapping")) - implementation("org.eclipse.tractusx.edc:tx-dcp:$txVersion") implementation(project(":edc-extensions:dynamic-issuers")) + implementation(project(":edc-extensions:basic-abac")) } tasks.withType { diff --git a/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-vault/build.gradle.kts b/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-vault/build.gradle.kts index 1e2c7e101a..e02583665d 100644 --- a/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-vault/build.gradle.kts +++ b/edc-controlplane/edc-controlplane-construct-x/con-x-controlplane-postgresql-vault/build.gradle.kts @@ -36,8 +36,8 @@ dependencies { implementation("org.eclipse.tractusx.edc:agreements:$txVersion") implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion") + implementation(project(":edc-extensions:basic-abac")) implementation(project(":edc-extensions:agreements:retirement-evaluation-bootstrapping")) - implementation("org.eclipse.tractusx.edc:tx-dcp:$txVersion") implementation(project(":edc-extensions:dynamic-issuers")) } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/environments/local-con-x-env.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/environments/local-con-x-env.bru index 16365f6d63..3ba5c16c63 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/environments/local-con-x-env.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/environments/local-con-x-env.bru @@ -6,12 +6,7 @@ vars { CONSUMER_IDHUB_ID_API: http://localhost:20100/api/identity CONSUMER_IDHUB_STS_API: http://localhost:20500/api/sts CONSUMER_IDHUB_CREDS_API: http://localhost:20600/api/credentials - ISSUER_APIKEY: Y29uLXgtaXNzdWVy.P3ROSh354pDFG8vOq4DB+lChxi4A6vIcbI/07UvwnKTIKl43pousUO0hu5fH28YnuQzbVF1meRBWjKU55/PT/g== - CONSUMER_IH_APIKEY: dXNlci1jb25zdW1lcg==.dMQICllhhPpgGtJBMWn2uKnH4+lkLAAD2PcPjn6iAIlMKBXUPmFHShj+HNYUDOY4zPQqwqBZ+cki4Mhtu7oyvw== - PROVIDER_IH_APIKEY: dXNlci1wcm92aWRlcg==.cdX1+2gWJtn9LKO/z2wkYwuybBhBRTQmFw5Se0CSvFq8ZEPSaDjskG15nmUx0iFKfXYEDase6yTEVlqQKoleQQ== VAULTURL: http://localhost:8200 - CONSUMER_STS_SECRET: fGznFfHH2skwmCJ4 - PROVIDER_STS_SECRET: DA9ujfJuoWgPPafQ PROVIDER_MANAGEMENT: http://localhost:39010/management CONSUMER_MANAGEMENT: http://localhost:29010/management PROVIDER_DATAPLANE_PUBLIC: http://localhost:9500/public @@ -22,14 +17,11 @@ vars { ISS_ID: did:web:local-issuer-wallet:con-x-issuer CONS_ID: did:web:consumer-wallet:user:consumer PROV_ID: did:web:provider-wallet:user:provider - cons_access_token: eyJraWQiOiJkaWQ6d2ViOmNvbnN1bWVyLXdhbGxldDp1c2VyOmNvbnN1bWVyI2tleS0xIiwiYWxnIjoiRWQyNTUxOSJ9.eyJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLXdhbGxldDp1c2VyOmNvbnN1bWVyIiwic3ViIjoiZGlkOndlYjpwcm92aWRlci13YWxsZXQ6dXNlcjpwcm92aWRlciIsIm5iZiI6MTc4ODI1NzYxOCwic2NvcGUiOiJvcmcuZWNsaXBzZS5kc3BhY2UuZGNwLnZjLnR5cGU6TWVtYmVyc2hpcENyZWRlbnRpYWw6cmVhZCIsImlzcyI6ImRpZDp3ZWI6Y29uc3VtZXItd2FsbGV0OnVzZXI6Y29uc3VtZXIiLCJleHAiOjE3ODgyNTc5MTgsImlhdCI6MTc4ODI1NzYxOCwianRpIjoiYWNjZXNzdG9rZW4tNjc1NDk4Y2QtOTQ3ZC00NjZmLWI0OTMtZjgxZTRhMjNlMDg3In0.su14qdbmn1zgBAbhSeamzEOC8Gk66pdb9XUFTQKuLRG3Om7MefQUzUTyjcNNzpqbUCGC2DV6h1jK6RmHfFdYAA - prov_access_token: eyJraWQiOiJkaWQ6d2ViOnByb3ZpZGVyLXdhbGxldDp1c2VyOnByb3ZpZGVyI2tleS0xIiwiYWxnIjoiRWQyNTUxOSJ9.eyJzdWIiOiJkaWQ6d2ViOnByb3ZpZGVyLXdhbGxldDp1c2VyOnByb3ZpZGVyIiwiYXVkIjoiZGlkOndlYjpjb25zdW1lci13YWxsZXQ6dXNlcjpjb25zdW1lciIsIm5iZiI6MTc4ODI1NzYxOSwiaXNzIjoiZGlkOndlYjpwcm92aWRlci13YWxsZXQ6dXNlcjpwcm92aWRlciIsImV4cCI6MTc4ODI1NzkxOSwiaWF0IjoxNzg4MjU3NjE5LCJqdGkiOiJhOTY5ODA4Zi1lZjU0LTRjNWEtOTE3YS05Mjk2NTc3YjQzYWUiLCJ0b2tlbiI6ImV5SnJhV1FpT2lKa2FXUTZkMlZpT21OdmJuTjFiV1Z5TFhkaGJHeGxkRHAxYzJWeU9tTnZibk4xYldWeUkydGxlUzB4SWl3aVlXeG5Jam9pUldReU5UVXhPU0o5LmV5SmhkV1FpT2lKa2FXUTZkMlZpT21OdmJuTjFiV1Z5TFhkaGJHeGxkRHAxYzJWeU9tTnZibk4xYldWeUlpd2ljM1ZpSWpvaVpHbGtPbmRsWWpwd2NtOTJhV1JsY2kxM1lXeHNaWFE2ZFhObGNqcHdjbTkyYVdSbGNpSXNJbTVpWmlJNk1UYzRPREkxTnpZeE9Dd2ljMk52Y0dVaU9pSnZjbWN1WldOc2FYQnpaUzVrYzNCaFkyVXVaR053TG5aakxuUjVjR1U2VFdWdFltVnljMmhwY0VOeVpXUmxiblJwWVd3NmNtVmhaQ0lzSW1semN5STZJbVJwWkRwM1pXSTZZMjl1YzNWdFpYSXRkMkZzYkdWME9uVnpaWEk2WTI5dWMzVnRaWElpTENKbGVIQWlPakUzT0RneU5UYzVNVGdzSW1saGRDSTZNVGM0T0RJMU56WXhPQ3dpYW5ScElqb2lZV05qWlhOemRHOXJaVzR0TmpjMU5EazRZMlF0T1RRM1pDMDBOalptTFdJME9UTXRaamd4WlRSaE1qTmxNRGczSW4wLnN1MTRxZGJtbjF6Z0JBYmhTZWFtekVPQzhHazY2cGRiOVhVRlRRS3VMUkczT203TWVmUVV6VVR5amNOTnpwcWJVQ0dDMkRWNmgxaks2Um1IZkZkWUFBIn0.-1wCoJjyw-KNOerivpCBmT0_hBV6wf2DySFScBKuN9HBt23AjckxNJ7NrQJNlfU5LlWf3RJr3jr6iXDlwryRDw - offerId: MQ==:YXNzZXRJZA==:N2RjZjc1OTktZTQzMy00NmY3LThlOTEtMWVjZTc3YzNlOWM1 - negotiation-id: 50dc06d7-bc96-40b1-9274-1eb7117d6dbb - transferId: 175f3962-2fc2-46ab-ba9e-1da78b7ad685 - pullSecret: eyJraWQiOiJwcm92X3B1YiIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJhbm9ueW1vdXMiLCJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLXdhbGxldDp1c2VyOmNvbnN1bWVyIiwic3ViIjoiYW5vbnltb3VzIiwiaWF0IjoxNzg4MjU3NjQxLCJqdGkiOiIxMzQwZmI3YS1lNDhjLTQ5ODQtYTEwZi0yODI0NGIwOTc2NjkifQ.RmjZ_Ac5iUoWnbRWm3Lz3ylSka5qPGXtAS_Ono1vzE-qUGerYKthdfXMmI0Qa11RLas2ZPbzaYctoHYvIVHoQkv9dYkjjffb_I48QgSMNIJmxOF9snagBmo6ehwMpd0GSO_B2fjeh_qYcLuiHofz8bp6Ff7N1K1dsPKeM5KsPrr1ClBoSzxWB1uCVsh8zM7QIhdliGuVvkPLcWHqIrks4smj4PfNMgkpZTE3PwEu-1LKKPY95_5cbaqPD1F9yeqL1rkr39ZJ-P0hlmVs13a5oSThl1wdtqijfpNeDPvgX-h0TJ4UCSSnTqO8aKCxyjVMhqfBsaz_mCI7VFrC9HOHBQ ISS_PART_CONT: con-x-issuer CONS_PART_CONT: user-consumer PROV_PART_CONT: user-provider - contractId: 6fba2299-5a7e-4bdf-b1b6-c0b609fde33c + MEM_CRED_ID: https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential + CUSTOM_CRED_ID: https://foo.org/constructx/bar/credentials/v1/Baustelle123Credential + TARGET_ASSET_ID: demoAssetIdOne + CRFORMAT: VC1_0_JWT } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowConsumerCredentials.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowConsumerCredentials.bru index 5886d0f70b..af7d10a50d 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowConsumerCredentials.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowConsumerCredentials.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{CONSUMER_IDHUB_ID_API}}/v1alpha/credentials + url: {{CONSUMER_IDHUB_ID_API}}/v1beta/credentials body: none auth: none } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowProviderCredentials.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowProviderCredentials.bru index 072de2cdd0..0d78ee9618 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowProviderCredentials.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Inspect Outcome/ShowProviderCredentials.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{PROVIDER_IDHUB_ID_API}}/v1alpha/credentials + url: {{PROVIDER_IDHUB_ID_API}}/v1beta/credentials body: none auth: none } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/CreateConsumerParticipant.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/CreateConsumerParticipant.bru index 6c2d065aa4..5e4731db28 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/CreateConsumerParticipant.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/CreateConsumerParticipant.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{CONSUMER_IDHUB_ID_API}}/v1alpha/participants + url: {{CONSUMER_IDHUB_ID_API}}/v1beta/participants body: json auth: inherit } @@ -26,7 +26,7 @@ body:json { { "id": "ConsumerIssuerService-ID", "type": "IssuerService", - "serviceEndpoint": "http://consumer-wallet:13132/api/issuance/v1alpha/participants/{{CONS_PART_CONT}}" + "serviceEndpoint": "http://consumer-wallet:13132/api/issuance/v1beta/participants/{{CONS_PART_CONT}}" } ], "active": true, @@ -52,11 +52,11 @@ script:pre-request { script:post-response { const apiKey = res.getBody().apiKey.trim(); if (apiKey) { - bru.setEnvVar("CONSUMER_IH_APIKEY", apiKey); + bru.setVar("CONSUMER_IH_APIKEY", apiKey); } const stsSecret = res.getBody().clientSecret.trim(); if (stsSecret) { - bru.setEnvVar("CONSUMER_STS_SECRET", stsSecret) + bru.setVar("CONSUMER_STS_SECRET", stsSecret) } } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/Get Consumer DID Doc.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/Get Consumer DID Doc.bru index 557b4b54a5..5a64175482 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/Get Consumer DID Doc.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/Get Consumer DID Doc.bru @@ -1,7 +1,7 @@ meta { name: Get Consumer DID Doc type: http - seq: 3 + seq: 4 } get { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestConsumerDevMemCredential.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestConsumerDevMemCredential.bru index 922b9e7b11..d65c70ae00 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestConsumerDevMemCredential.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestConsumerDevMemCredential.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{CONSUMER_IDHUB_ID_API}}/v1alpha/participants/{{CONS_PART_CONT}}/credentials/request + url: {{CONSUMER_IDHUB_ID_API}}/v1beta/participants/{{CONS_PART_CONT}}/credentials/request body: json auth: none } @@ -14,8 +14,8 @@ body:json { { "issuerDid": "{{ISS_ID}}", "credentials": [{ - "format": "VC1_0_JWT", - "type": "MembershipCredential", + "format": "{{CRFORMAT}}", + "type": "{{MEM_CRED_ID}}", "id": "dev-credential-def-1" }] } @@ -25,7 +25,7 @@ script:pre-request { const cons_id = bru.getEnvVar("CONS_ID"); const participantContext = cons_id.split(":").slice(3).join(":").replace(/:/g, "-"); bru.setEnvVar("CONS_PART_CONT", participantContext); - req.setHeader("x-api-key", bru.getEnvVar("CONSUMER_IH_APIKEY")); + req.setHeader("x-api-key", bru.getVar("CONSUMER_IH_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestCustomCredential.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestCustomCredential.bru new file mode 100644 index 0000000000..cb905f720e --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/RequestCustomCredential.bru @@ -0,0 +1,34 @@ +meta { + name: RequestCustomCredential + type: http + seq: 3 +} + +post { + url: {{CONSUMER_IDHUB_ID_API}}/v1beta/participants/{{CONS_PART_CONT}}/credentials/request + body: json + auth: none +} + +body:json { + { + "issuerDid": "{{ISS_ID}}", + "credentials": [{ + "format": "{{CRFORMAT}}", + "type": "{{CUSTOM_CRED_ID}}", + "id": "customCredentialTypeId" + }] + } +} + +script:pre-request { + const cons_id = bru.getEnvVar("CONS_ID"); + const participantContext = cons_id.split(":").slice(3).join(":").replace(/:/g, "-"); + bru.setEnvVar("CONS_PART_CONT", participantContext); + req.setHeader("x-api-key", bru.getVar("CONSUMER_IH_APIKEY")); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret test.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret test.bru index 6e3a7833dc..62a9d72f22 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret test.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret test.bru @@ -1,7 +1,7 @@ meta { name: VaultSecret test type: http - seq: 5 + seq: 6 } get { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret.bru index b12519e226..bd91536f39 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Consumer ID/VaultSecret.bru @@ -1,7 +1,7 @@ meta { name: VaultSecret type: http - seq: 4 + seq: 5 } post { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/CreateIssuerParticipant.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/CreateIssuerParticipant.bru index 380ab5c193..ff1d5055c2 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/CreateIssuerParticipant.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/CreateIssuerParticipant.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{ISSUER_ID_API}}/v1alpha/participants + url: {{ISSUER_ID_API}}/v1beta/participants body: json auth: inherit } @@ -21,7 +21,7 @@ body:json { { "id": "IssuerIssuerService-ID", "type": "IssuerService", - "serviceEndpoint": "http://local-issuer-wallet:13132/api/issuance/v1alpha/participants/{{ISS_PART_CONT}}" + "serviceEndpoint": "http://local-issuer-wallet:13132/api/issuance/v1beta/participants/{{ISS_PART_CONT}}" }, { "id": "IssuerCredentialService-ID", @@ -52,7 +52,7 @@ script:pre-request { script:post-response { const apiKey = res.getBody().apiKey.trim(); if (apiKey) { - bru.setEnvVar("ISSUER_APIKEY", apiKey); + bru.setVar("ISSUER_APIKEY", apiKey); } } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addConsumerHolder.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addConsumerHolder.bru index 11f36cebcf..414e56ad2e 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addConsumerHolder.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addConsumerHolder.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{ISSUER_ISS_API}}/v1alpha/participants/{{ISS_PART_CONT}}/holders + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/holders body: json auth: inherit } @@ -23,7 +23,7 @@ script:pre-request { const participantContext = cons_id.split(":").slice(3).join(":").replace(/:/g, "-"); bru.setEnvVar("CONS_PART_CONT", participantContext); - req.setHeader("x-api-key", bru.getEnvVar("ISSUER_APIKEY")); + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addProviderHolder.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addProviderHolder.bru index e10bfc6ac6..8c886e8048 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addProviderHolder.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/addProviderHolder.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{ISSUER_ISS_API}}/v1alpha/participants/{{ISS_PART_CONT}}/holders + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/holders body: json auth: inherit } @@ -23,7 +23,7 @@ script:pre-request { const participantContext = prov_id.split(":").slice(3).join(":").replace(/:/g, "-"); bru.setEnvVar("PROV_PART_CONT", participantContext); - req.setHeader("x-api-key", bru.getEnvVar("ISSUER_APIKEY")); + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomAttestation.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomAttestation.bru new file mode 100644 index 0000000000..435198d6a5 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomAttestation.bru @@ -0,0 +1,42 @@ +meta { + name: createCustomAttestation + type: http + seq: 7 +} + +post { + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/attestations + body: json + auth: none +} + +headers { + ~x-api-key: {{ISSUER_APIKEY}} +} + +body:json { + { + "attestationType": "dev", + "id": "customAttestationID", + "configuration": { + "default": { + "accessLevels": { + "fooLevel": 3, + "barLevel": { + "role": "Sheriff" + } + } + }, + "blackList": [] + } + } +} + +script:pre-request { + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomCredentialDef.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomCredentialDef.bru new file mode 100644 index 0000000000..890744f183 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createCustomCredentialDef.bru @@ -0,0 +1,42 @@ +meta { + name: createCustomCredentialDef + type: http + seq: 8 +} + +post { + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/credentialdefinitions + body: json + auth: inherit +} + +body:json { + { + "attestations": [ + "customAttestationID" + ], + "credentialType": "{{CUSTOM_CRED_ID}}", + "id": "customCredentialTypeId", + "jsonSchema": "{}", + "jsonSchemaUrl": "https://example.com/schema/dev-credential.json", + "mappings": [ + { + "input": "content", + "output": "credentialSubject", + "required": true + } + ], + "rules": [], + "format": "{{CRFORMAT}}", + "validity": 15552000 + } +} + +script:pre-request { + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevAttestation.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevAttestation.bru index 1e9801e71c..a6dfeda201 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevAttestation.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevAttestation.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{ISSUER_ISS_API}}/v1alpha/participants/{{ISS_PART_CONT}}/attestations + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/attestations body: json auth: none } @@ -19,26 +19,8 @@ body:json { "attestationType": "dev", "id": "dev-def-1", "configuration": { - "{{CONS_ID}}": { - "isConsumer": true, - "isProvider": false, - "foo": { - "bar": 123 - } - }, - "{{PROV_ID}}": { - "isConsumer": false, - "isProvider": true, - "foo": { - "bar": 789 - } - }, "default": { - "isConsumer": false, - "isProvider": false, - "foo": { - "bar": 0 - } + "isMember": true }, "blackList": [] } @@ -46,7 +28,7 @@ body:json { } script:pre-request { - req.setHeader("x-api-key", bru.getEnvVar("ISSUER_APIKEY")); + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevMemCredentialDef.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevMemCredentialDef.bru index fe36fe1f4e..43752c2a08 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevMemCredentialDef.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Issuer/createDevMemCredentialDef.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{ISSUER_ISS_API}}/v1alpha/participants/{{ISS_PART_CONT}}/credentialdefinitions + url: {{ISSUER_ISS_API}}/v1beta/participants/{{ISS_PART_CONT}}/credentialdefinitions body: json auth: inherit } @@ -15,7 +15,7 @@ body:json { "attestations": [ "dev-def-1" ], - "credentialType": "MembershipCredential", + "credentialType": "{{MEM_CRED_ID}}", "id": "dev-credential-def-1", "jsonSchema": "{}", "jsonSchemaUrl": "https://example.com/schema/dev-credential.json", @@ -27,13 +27,13 @@ body:json { } ], "rules": [], - "format": "VC1_0_JWT", + "format": "{{CRFORMAT}}", "validity": 15552000 } } script:pre-request { - req.setHeader("x-api-key", bru.getEnvVar("ISSUER_APIKEY")); + req.setHeader("x-api-key", bru.getVar("ISSUER_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/CreateProviderParticipant.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/CreateProviderParticipant.bru index d37af1d728..066b5ca69f 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/CreateProviderParticipant.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/CreateProviderParticipant.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{PROVIDER_IDHUB_ID_API}}/v1alpha/participants + url: {{PROVIDER_IDHUB_ID_API}}/v1beta/participants body: json auth: inherit } @@ -26,7 +26,7 @@ body:json { { "id": "ProviderIssuerService-ID", "type": "IssuerService", - "serviceEndpoint": "http://provider-wallet:13132/api/issuance/v1alpha/participants/{{PROV_PART_CONT}}" + "serviceEndpoint": "http://provider-wallet:13132/api/issuance/v1beta/participants/{{PROV_PART_CONT}}" } ], "active": true, @@ -52,11 +52,11 @@ script:pre-request { script:post-response { const apiKey = res.getBody().apiKey.trim(); if (apiKey) { - bru.setEnvVar("PROVIDER_IH_APIKEY", apiKey); + bru.setVar("PROVIDER_IH_APIKEY", apiKey); } const stsSecret = res.getBody().clientSecret.trim(); if (stsSecret) { - bru.setEnvVar("PROVIDER_STS_SECRET", stsSecret) + bru.setVar("PROVIDER_STS_SECRET", stsSecret) } } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/Get Provider DID Doc.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/Get Provider DID Doc.bru index 0187613b95..f8e5ded11c 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/Get Provider DID Doc.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/Get Provider DID Doc.bru @@ -1,7 +1,7 @@ meta { name: Get Provider DID Doc type: http - seq: 3 + seq: 4 } get { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestCustomCredential.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestCustomCredential.bru new file mode 100644 index 0000000000..d1f5103241 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestCustomCredential.bru @@ -0,0 +1,35 @@ +meta { + name: RequestCustomCredential + type: http + seq: 3 +} + +post { + url: {{PROVIDER_IDHUB_ID_API}}/v1beta/participants/{{PROV_PART_CONT}}/credentials/request + body: json + auth: none +} + +body:json { + { + "issuerDid": "{{ISS_ID}}", + "credentials": [{ + "format": "{{CRFORMAT}}", + "type": "{{CUSTOM_CRED_ID}}", + "id": "customCredentialTypeId" + }] + } +} + +script:pre-request { + const prov_id = bru.getEnvVar("PROV_ID"); + const participantContext = prov_id.split(":").slice(3).join(":").replace(/:/g, "-"); + bru.setEnvVar("PROV_PART_CONT", participantContext); + + req.setHeader("x-api-key", bru.getVar("PROVIDER_IH_APIKEY")); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestProviderDevMemCredential.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestProviderDevMemCredential.bru index b71fd56e0c..47a2f15fcb 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestProviderDevMemCredential.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/RequestProviderDevMemCredential.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{PROVIDER_IDHUB_ID_API}}/v1alpha/participants/{{PROV_PART_CONT}}/credentials/request + url: {{PROVIDER_IDHUB_ID_API}}/v1beta/participants/{{PROV_PART_CONT}}/credentials/request body: json auth: none } @@ -14,8 +14,8 @@ body:json { { "issuerDid": "{{ISS_ID}}", "credentials": [{ - "format": "VC1_0_JWT", - "type": "MembershipCredential", + "format": "{{CRFORMAT}}", + "type": "{{MEM_CRED_ID}}", "id": "dev-credential-def-1" }] } @@ -26,7 +26,7 @@ script:pre-request { const participantContext = prov_id.split(":").slice(3).join(":").replace(/:/g, "-"); bru.setEnvVar("PROV_PART_CONT", participantContext); - req.setHeader("x-api-key", bru.getEnvVar("PROVIDER_IH_APIKEY")); + req.setHeader("x-api-key", bru.getVar("PROVIDER_IH_APIKEY")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret test.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret test.bru index 03f3b64282..e2b7afd47b 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret test.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret test.bru @@ -1,7 +1,7 @@ meta { name: VaultSecret test type: http - seq: 5 + seq: 6 } get { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret.bru index b05fd6d48a..b68a9df128 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Prepare Provider ID/VaultSecret.bru @@ -1,7 +1,7 @@ meta { name: VaultSecret type: http - seq: 4 + seq: 5 } post { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Consumer Token.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Consumer Token.bru index 16b2325aa7..f0dbf38065 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Consumer Token.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Consumer Token.bru @@ -15,7 +15,7 @@ body:form-urlencoded { client_secret: {{CONSUMER_STS_SECRET}} client_id: {{CONS_ID}} audience: {{PROV_ID}} - bearer_access_scope: org.eclipse.dspace.dcp.vc.type:MembershipCredential:read + bearer_access_scope: org.eclipse.dspace.dcp.vc.type:{{MEM_CRED_ID}}:read } script:post-response { @@ -26,7 +26,7 @@ script:post-response { const payloadObject = JSON.parse(payload); const internalToken = payloadObject.token.trim(); - bru.setEnvVar("cons_access_token", internalToken); + bru.setVar("cons_access_token", internalToken); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Get Credential.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Get Credential.bru index 8ea42d7663..57d6e22f29 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Get Credential.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Get Credential.bru @@ -23,7 +23,7 @@ body:json { "type": "PresentationQueryMessage", "presentationDefinition": null, "scope": [ - "org.eclipse.dspace.dcp.vc.type:MembershipCredential:read" + "org.eclipse.dspace.dcp.vc.type:{{MEM_CRED_ID}}:read" ] } } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Provider Token.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Provider Token.bru index 3ec92f6e2c..7edb4bc020 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Provider Token.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/identities/Simulated DCP Flow/Provider Token.bru @@ -21,7 +21,7 @@ body:form-urlencoded { script:post-response { const accessToken = res.getBody().access_token.trim(); - bru.setEnvVar("prov_access_token", accessToken); + bru.setVar("prov_access_token", accessToken); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/CheckNegotiationResult.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/CheckNegotiationResult.bru index d475666650..6fa3278388 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/CheckNegotiationResult.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/CheckNegotiationResult.bru @@ -16,7 +16,7 @@ headers { script:post-response { const contractId = res.getBody()['contractAgreementId']; - bru.setEnvVar("contractId", contractId); + bru.setVar("contractId", contractId); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/Get EDR.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/Get EDR.bru index cc00b204b0..7a50665e7a 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/Get EDR.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/Get EDR.bru @@ -16,7 +16,7 @@ headers { script:post-response { const authToken = res.getBody().authorization; - bru.setEnvVar("pullSecret", authToken); + bru.setVar("pullSecret", authToken); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitPullTransfer.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitPullTransfer.bru index b7635d665c..f394160806 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitPullTransfer.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitPullTransfer.bru @@ -26,7 +26,7 @@ body:json { script:post-response { const transferId = res.getBody()['@id']; - bru.setEnvVar("transferId", transferId); + bru.setVar("transferId", transferId); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitiateNegotiation.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitiateNegotiation.bru index 80e72fa844..11cf82a57e 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitiateNegotiation.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/InitiateNegotiation.bru @@ -28,9 +28,10 @@ body:json { "@context": "http://www.w3.org/ns/odrl.jsonld", "@id": "{{offerId}}", "@type": "Offer", + "permission": {{catalogPermission}}, "assigner": "{{PROV_ID}}", "assignee": "{{CONS_ID}}", - "target": "assetId" + "target": "{{TARGET_ASSET_ID}}" } } } @@ -38,7 +39,7 @@ body:json { script:post-response { var x = res.getBody()['@id']; console.log("id " + x); - bru.setEnvVar("negotiation-id", res.getBody()['@id']); + bru.setVar("negotiation-id", res.getBody()['@id']); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/PullAssetData.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/PullAssetData.bru index df33451a12..11b51d2670 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/PullAssetData.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/PullAssetData.bru @@ -11,7 +11,7 @@ get { } script:pre-request { - req.setHeader("Authorization", bru.getEnvVar("pullSecret")); + req.setHeader("Authorization", bru.getVar("pullSecret")); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/RequestProviderCatalog.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/RequestProviderCatalog.bru index 128f08a1c7..e95eb423be 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/RequestProviderCatalog.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/consumer/RequestProviderCatalog.bru @@ -22,17 +22,32 @@ body:json { } script:post-response { - const offerArray = res.getBody().dataset[0].hasPolicy; - var offerId; - res.getBody().dataset[0].hasPolicy.forEach(item => { - const type = item['@type']; - const id = item['@id']; - if (type != null && type == "Offer" && id != null) { - offerId = id; - } - }); - bru.setEnvVar("offerId", offerId); + const targetAssetId = bru.getEnvVar("TARGET_ASSET_ID"); + + var offerId; + var permissionArray; + var foundIt = false; + + res.getBody().dataset.forEach(dataset => { + if (dataset['@id'] == targetAssetId) { + dataset.hasPolicy.forEach(item => { + const type = item['@type']; + const id = item['@id']; + if (type == "Offer" && id != null) { + offerId = id; + permissionArray = item.permission; + foundIt = true; + } + }); + } + }); + if (!foundIt) { + offerId = ""; + permissionArray = []; + } + bru.setVar("offerId", offerId); + bru.setVar("catalogPermission", JSON.stringify(permissionArray)); } settings { diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateContractDefinition.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateContractDefinition.bru deleted file mode 100644 index e7b3b069fa..0000000000 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateContractDefinition.bru +++ /dev/null @@ -1,28 +0,0 @@ -meta { - name: CreateContractDefinition - type: http - seq: 3 -} - -post { - url: {{PROVIDER_MANAGEMENT}}/v3/contractdefinitions - body: json - auth: inherit -} - -body:json { - { - "@context": { - "@vocab": "https://w3id.org/edc/v0.0.1/ns/" - }, - "@id": "1", - "accessPolicyId": "aPolicy", - "contractPolicyId": "aPolicy", - "assetsSelector": [] - } -} - -settings { - encodeUrl: true - timeout: 0 -} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAccessPolicy.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAccessPolicy.bru new file mode 100644 index 0000000000..287dc59bdf --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAccessPolicy.bru @@ -0,0 +1,46 @@ +meta { + name: CreateAccessPolicy + type: http + seq: 2 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/policydefinitions + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/", + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "@id": "accessPolicyOne", + "policy": { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Set", + "permission": [ + { + "action": "http://www.w3.org/ns/odrl/2/use", + "constraint": { + "and": [ + { + "leftOperand": "{{MEM_CRED_ID}}.credentialSubject.isMember", + "operator": "isAnyOf", + "rightOperand": "[true]" + } + ] + } + } + ], + "prohibition": [], + "obligation": [] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateAsset.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAsset.bru similarity index 95% rename from edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateAsset.bru rename to edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAsset.bru index df3fea69fc..d30aeddc22 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreateAsset.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateAsset.bru @@ -15,7 +15,7 @@ body:json { "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" }, - "@id": "assetId", + "@id": "demoAssetIdOne", "properties": { "name": "product description", "contenttype": "application/json" diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractDefinition.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractDefinition.bru new file mode 100644 index 0000000000..d5a812fac5 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractDefinition.bru @@ -0,0 +1,35 @@ +meta { + name: CreateContractDefinition + type: http + seq: 4 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/contractdefinitions + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@id": "contractDefOne", + "accessPolicyId": "accessPolicyOne", + "contractPolicyId": "contractPolicyOne", + "assetsSelector": { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator": "in", + "operandRight": [ + "demoAssetIdOne" + ] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractPolicy.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractPolicy.bru new file mode 100644 index 0000000000..06a62bf800 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/CreateContractPolicy.bru @@ -0,0 +1,46 @@ +meta { + name: CreateContractPolicy + type: http + seq: 3 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/policydefinitions + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/", + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "@id": "contractPolicyOne", + "policy": { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Set", + "permission": [ + { + "action": "http://www.w3.org/ns/odrl/2/use", + "constraint": { + "and": [ + { + "leftOperand": "{{CUSTOM_CRED_ID}}.credentialSubject.accessLevels.fooLevel", + "operator": "isAnyOf", + "rightOperand": "[2, 3, 4, 5]" + } + ] + } + } + ], + "prohibition": [], + "obligation": [] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/folder.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/folder.bru new file mode 100644 index 0000000000..577a231927 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetOne/folder.bru @@ -0,0 +1,8 @@ +meta { + name: createAssetOne + seq: 1 +} + +auth { + mode: inherit +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreatePolicy.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAccessPolicy.bru similarity index 50% rename from edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreatePolicy.bru rename to edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAccessPolicy.bru index ede9c2a378..65778efd39 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/CreatePolicy.bru +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAccessPolicy.bru @@ -1,5 +1,5 @@ meta { - name: CreatePolicy + name: CreateAccessPolicy type: http seq: 2 } @@ -16,11 +16,24 @@ body:json { "@vocab": "https://w3id.org/edc/v0.0.1/ns/", "odrl": "http://www.w3.org/ns/odrl/2/" }, - "@id": "aPolicy", + "@id": "accessPolicyTwo", "policy": { "@context": "http://www.w3.org/ns/odrl.jsonld", "@type": "Set", - "permission": [], + "permission": [ + { + "action": "http://www.w3.org/ns/odrl/2/use", + "constraint": { + "and": [ + { + "leftOperand": "{{MEM_CRED_ID}}.credentialSubject.isMember", + "operator": "eq", + "rightOperand": true + } + ] + } + } + ], "prohibition": [], "obligation": [] } diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAsset.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAsset.bru new file mode 100644 index 0000000000..03e3fae9ee --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateAsset.bru @@ -0,0 +1,39 @@ +meta { + name: CreateAsset + type: http + seq: 1 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/assets + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@id": "demoAssetIdTwo", + "properties": { + "name": "product description", + "contenttype": "application/json" + }, + "dataAddress": { + "type": "HttpData", + "name": "Test asset", + "baseUrl": "https://jsonplaceholder.typicode.com/users", + "proxyPath": "true", + "proxyMethod": "true", + "proxyBody": "true", + "authKey": "x-api-key", + "authCode": "someAuthCode" + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractDefinition.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractDefinition.bru new file mode 100644 index 0000000000..f35c4f1e6c --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractDefinition.bru @@ -0,0 +1,35 @@ +meta { + name: CreateContractDefinition + type: http + seq: 4 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/contractdefinitions + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@id": "contractDefTwo", + "accessPolicyId": "accessPolicyTwo", + "contractPolicyId": "contractPolicyTwo", + "assetsSelector": { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator": "in", + "operandRight": [ + "demoAssetIdTwo" + ] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractPolicy.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractPolicy.bru new file mode 100644 index 0000000000..910719efc4 --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/CreateContractPolicy.bru @@ -0,0 +1,46 @@ +meta { + name: CreateContractPolicy + type: http + seq: 3 +} + +post { + url: {{PROVIDER_MANAGEMENT}}/v3/policydefinitions + body: json + auth: inherit +} + +body:json { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/", + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "@id": "contractPolicyTwo", + "policy": { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Set", + "permission": [ + { + "action": "http://www.w3.org/ns/odrl/2/use", + "constraint": { + "and": [ + { + "leftOperand": "{{CUSTOM_CRED_ID}}.credentialSubject.accessLevels.barLevel.role", + "operator": "isAnyOf", + "rightOperand": "[\"Sheriff\", \"Marshal\"]" + } + ] + } + } + ], + "prohibition": [], + "obligation": [] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/folder.bru b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/folder.bru new file mode 100644 index 0000000000..b15fc1c55a --- /dev/null +++ b/edc-controlplane/edc-controlplane-construct-x/local/bruno/con-x-local-test/transactions/provider/createAssetTwo/folder.bru @@ -0,0 +1,8 @@ +meta { + name: createAssetTwo + seq: 2 +} + +auth { + mode: inherit +} diff --git a/edc-controlplane/edc-controlplane-construct-x/local/docker-compose.yaml b/edc-controlplane/edc-controlplane-construct-x/local/docker-compose.yaml index 5a1b5352df..6403310355 100644 --- a/edc-controlplane/edc-controlplane-construct-x/local/docker-compose.yaml +++ b/edc-controlplane/edc-controlplane-construct-x/local/docker-compose.yaml @@ -21,7 +21,7 @@ services: local-issuer-wallet: container_name: local-issuer-wallet - image: ghcr.io/project-construct-x/wallet:0.17.0-1 + image: ghcr.io/project-construct-x/wallet-sql-vault:0.18.0-1 pull_policy: missing depends_on: shared-postgres: @@ -121,7 +121,7 @@ services: consumer-wallet: container_name: consumer-wallet - image: ghcr.io/project-construct-x/wallet:0.17.0-1 + image: ghcr.io/project-construct-x/wallet-sql-vault:0.18.0-1 pull_policy: missing depends_on: shared-postgres: @@ -162,7 +162,7 @@ services: provider-wallet: container_name: provider-wallet - image: ghcr.io/project-construct-x/wallet:0.17.0-1 + image: ghcr.io/project-construct-x/wallet-sql-vault:0.18.0-1 pull_policy: missing depends_on: shared-postgres: @@ -207,7 +207,7 @@ services: pull_policy: never environment: - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005 - - edc.iam.trusted-issuer.example.id=did:web:local-issuer-wallet:con-x-issuer + - edc.iam.trustedissuer.example.id=did:web:local-issuer-wallet:con-x-issuer - edc.iam.did.web.use.https=false - edc.iam.sts.oauth.client.secret.alias=consumersecret - edc.iam.credential.revocation.mimetype=application/json @@ -236,9 +236,7 @@ services: - edc.vault.hashicorp.url=http://shared-vault:8200 - edc.vault.hashicorp.health.check.enabled=true - edc.vault.hashicorp.token=vaultsecret0123456789 - - tx.edc.iam.dcp.default-scopes.test.alias=org.eclipse.dspace.dcp.vc.type - - tx.edc.iam.dcp.default-scopes.test.type=MembershipCredential - - tx.edc.iam.dcp.default-scopes.test.operation=read + healthcheck: test: ["CMD-SHELL", "wget --spider http://localhost:9000/api/check/readiness || exit 1"] start_period: 10s @@ -305,7 +303,7 @@ services: pull_policy: never environment: - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005 - - edc.iam.trusted-issuer.example.id=did:web:local-issuer-wallet:con-x-issuer + - edc.iam.trustedissuer.example.id=did:web:local-issuer-wallet:con-x-issuer - edc.iam.did.web.use.https=false - edc.iam.sts.oauth.client.secret.alias=providersecret - edc.iam.credential.revocation.mimetype=application/json @@ -334,9 +332,6 @@ services: - edc.vault.hashicorp.url=http://shared-vault:8200 - edc.vault.hashicorp.health.check.enabled=true - edc.vault.hashicorp.token=vaultsecret0123456789 - - tx.edc.iam.dcp.default-scopes.test.alias=org.eclipse.dspace.dcp.vc.type - - tx.edc.iam.dcp.default-scopes.test.type=MembershipCredential - - tx.edc.iam.dcp.default-scopes.test.operation=read healthcheck: test: [ "CMD-SHELL", "wget --spider http://localhost:9000/api/check/readiness || exit 1" ] start_period: 10s diff --git a/edc-extensions/basic-abac/README.md b/edc-extensions/basic-abac/README.md new file mode 100644 index 0000000000..e913da832f --- /dev/null +++ b/edc-extensions/basic-abac/README.md @@ -0,0 +1,174 @@ +# Basic Abac Extension (draft version) + +This extension allows you to create policy definitions that enable attribute-based access control. + +That means you have to specify a credential type by setting a `leftOperand` that designates a fully qualified credential type identifier. The identifier must be followed by `.credentialSubject` and one or more JSONPath segments that allow navigating through the credential subject used in that credential type. Please note that this extension will (at least currently) only work as expected, if you are using constraints within +the `permission` of a policy definition. + +Consider the following example: + +```json +{ + "permission": [ + { + "action": "use", + "constraint": [ + { + "and": [ + { + "leftOperand": "https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential.credentialSubject.isMember", + "operator": "eq", + "rightOperand": "true" + }, + { + "leftOperand": "https://w3id.org/constructx/credentials/v1.0/Baustelle123Credential.credentialSubject.accessLevel", + "operator": "gteq", + "rightOperand": 4 + } + ] + } + ] + } + ] +} +``` + +Here, the first `leftOperand` is: + +```text +https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential.credentialSubject.isMember +``` + +The last path segment consists of: + +1. The complete credential type, in this case `ConstructXMembershipCredential` +2. The mandatory `.credentialSubject` segment +3. One or more JSONPath segments, in this case `.isMember` + +### Pattern matching + +The `leftOperand` of a policy definition must match the regex pattern currently defined in the [BasicAbacUtils](./src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacUtils.java) class: + +```java +public static final String BASIC_ABAC_REGEX = + "^https://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}(?::[0-9]+)?(?:/[^\\s/]+)*/[A-Z][^\\s/.]*\\.credentialSubject(?:\\.[^\\s/.]+)+$"; +``` + +In essence, this regex matches if the target string: + +- is a valid HTTPS URL, +- has a last path segment that starts with a capital letter, +- contains `.credentialSubject` directly after the complete credential type, and +- contains one or more JSONPath segments after `.credentialSubject`. + +The JSONPath segments following .credentialSubject specify the path to a value within the credential subject of the corresponding verifiable credential. + +#### Examples matching the regex + +- `https://w3id.org/constructx/credentials/v1.0/Foo.credentialSubject.fooLevel` +- `https://w3id.org/constructx/credentials/v1.0/Bar.credentialSubject.nestedObject.barLevel` +- `https://my-domain.com:8080/some/arbitrary/path/segments/Foo.credentialSubject.fooLevel` +- `https://w3id.org/constructx/policies/v1.0/Foo.credentialSubject.fooLevel` + +#### Examples not matching the regex + +- `https://w3id.org/constructx/credentials/v1.0/Foo` + Missing `.credentialSubject` and a JSONPath segment. + +- `https://w3id.org/constructx/credentials/v1.0/Foo.credentialSubject` + Missing a JSONPath segment after `.credentialSubject`. + +- `https://w3id.org/constructx/credentials/v1.0/Foo.fooLevel` + Missing the mandatory `.credentialSubject` segment. + +- `http://example.org/credentials/Foo.credentialSubject.fooLevel` + Uses HTTP instead of HTTPS. + +- `https://w3id.org/constructx/credentials/v1.0/moo.credentialSubject.mooLevel` + The last path segment does not start with a capital letter. + +#### Understanding the JSONPath suffix + +To create sensible ABAC policies, you need to be aware of the structure of the credential subject in the verifiable credential. + +Consider the following `leftOperand`: + +```text +https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential.credentialSubject.isMember +``` + +The corresponding credential subject could be structured as follows: + +```json +{ + "id": "did:web:consumer-wallet:user:consumer", + "isMember": "true" +} +``` + +The mandatory `.credentialSubject` segment identifies the credential subject. The following JSONPath segment `.isMember` points to the value of the `isMember` field, which is the string `"true"` in this example. Just b.t.w.: Note that a JSON literal `true` would have also worked here (more on that below). + +Since the policy definition expects that boolean value to be equal to `true`, this condition is fulfilled. + +Now consider the second ABAC policy condition: + +```text +https://w3id.org/constructx/credentials/v1.0/Baustelle123Credential.credentialSubject.accessLevel +``` + +Assume the consumer participant has this credential, but the trusted issuer provided it with the following credential subject: + +```json +{ + "id": "did:web:consumer-wallet:user:consumer", + "accessLevel": "3" +} +``` + +The evaluation determines that an `accessLevel` of `4` is required, while only level `3` was granted. Therefore, the consumer participant is denied access when attempting to negotiate the corresponding contract offer. + +#### Equality of values + +This extension chooses to be rather generous when it comes to comparing different variants of representations of numeric or boolean values. + +That means for example + +- a string containing five, i.e. `"5"` is equal to a numeric `5`. And both are equal to a decimal `5.0`. +- a string whose content sort of looks like a boolean, e.g. `"true"`, `"FaLSE"` or `"TRUE"` will be interpreted as the corresponding boolean values `true` or `false` respectively. I.e. there is no case-sensitivity here. + +#### Constraints with set-operators + +This extension can also handle odrl set operators, like for instance `isPartOf`, `isNoneOf`, etc. In these cases, the rightOperand is expected to be structured as a JSON list. However, due to some technical reasons, it is not possible to write such a list like a normal JSON list in the context of a "create policy-definition call" at the EDC-management API. I.e. something like this + +```json + { + "leftOperand": "https://w3id.org/constructx/credentials/v1.0/Baustelle123Credential.credentialSubject.accessLevels.barLevel.role", + "operator": "isAnyOf", + "rightOperand": ["Sheriff", "Marshal"] + } +``` + +should always be avoided. Note that in such cases, the EDC-management API will NOT react with a status code 400. But any policy that was created in such a way, will almost certainly not work as intended. + +Instead, you need to use the "stringified" version of such a JSON list like this: + +```json + { + "leftOperand": "https://w3id.org/constructx/credentials/v1.0/Baustelle123Credential.credentialSubject.accessLevels.barLevel.role", + "operator": "isAnyOf", + "rightOperand": "[\"Sheriff\", \"Marshal\"]" + } +``` + +I.e. the rightOperand formally is always a JSON string, whose content can be parsed as a JSON list. Note that "internal" quotation marks need to be escaped like in the example above. + +#### Default Credential + +There is a technical necessity that the controlplane minimally needs at least one credential type it will expect or show to external partners. This is especially relevant when there is no credential-specific policy context, from which any credential types could possibly get extracted, i.e. when someone is trying to request someone else's EDC catalog. It can be defined with a property: + +``` +edc.abac.defaultcredential=https://w3id.org/my/very/special/FooMembershipCredential +``` + +You are not required to use that property. If you don't, it will default to `https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential`. + diff --git a/edc-extensions/basic-abac/build.gradle.kts b/edc-extensions/basic-abac/build.gradle.kts new file mode 100644 index 0000000000..c411caadcf --- /dev/null +++ b/edc-extensions/basic-abac/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * + * 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. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id("java") + id("application") +} + +repositories { mavenCentral() } + +val edcVersion = project.property("con-x-edcVersion") as String + +dependencies { + implementation("org.eclipse.edc:core-spi:${edcVersion}") + implementation("org.eclipse.edc:policy-engine-spi:${edcVersion}") + implementation("org.eclipse.edc:policy-spi:${edcVersion}") + implementation("org.eclipse.edc:participant-spi:${edcVersion}") + implementation("org.eclipse.edc:request-policy-context-spi:${edcVersion}") + implementation("org.eclipse.edc:catalog-spi:${edcVersion}") + implementation("org.eclipse.edc:contract-spi:${edcVersion}") + implementation("org.eclipse.edc:verifiable-credentials-spi:${edcVersion}") + + testImplementation("org.eclipse.edc:junit:${edcVersion}") { + exclude(group = "org.junit.jupiter") + exclude(group = "org.junit.platform") + exclude(group = "org.junit") + } +} + diff --git a/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacCredentialConstraintFunction.java b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacCredentialConstraintFunction.java new file mode 100644 index 0000000000..e9901adcf8 --- /dev/null +++ b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacCredentialConstraintFunction.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.fraunhofer.isst.edc.extension.basic_abac.dev; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.participant.spi.ParticipantAgentPolicyContext; +import org.eclipse.edc.policy.engine.spi.DynamicAtomicConstraintRuleFunction; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +import static de.fraunhofer.isst.edc.extension.basic_abac.dev.BasicAbacUtils.*; + +public class BasicAbacCredentialConstraintFunction implements DynamicAtomicConstraintRuleFunction { + + @Override + public boolean evaluate(Object leftValue, Operator operator, Object rightValue, Permission rule, C context) { + if (leftValue == null || rightValue == null || operator == null || context == null) { + return false; + } + + // case: is rightValue a list? + List rightValueList; + if (rightValue instanceof List list) { + rightValueList = normalizeNumericToDoubleOrBoolean(list); + } else { + rightValueList = convertJsonToList(rightValue.toString()); + } + if (rightValueList != null) { + return handleRightValueList(leftValue, operator, rightValueList, rule, context); + } + + + // case: is rightValue a (single) numeric? + try { + double numericValue = Double.parseDouble(rightValue.toString()); + if (!Double.isFinite(numericValue)) { + // always reject "NaN" or "Infinity" + return false; + } + return handleRightValueNumeric(leftValue, operator, numericValue, rule, context); + } catch (NumberFormatException e) { + } + + // case: rightValue is neither list nor numeric, but could be e.g. a Boolean or non-numeric String value + Object claimValue = extractValueFromCredentialSubject(context, leftValue); + if (claimValue == null) return false; + + // try to normalize Boolean values + claimValue = normalizeToBoolean(claimValue); + rightValue = normalizeToBoolean(rightValue); + + return switch (operator) { + case EQ -> rightValue.equals(claimValue); + case NEQ -> !rightValue.equals(claimValue); + case HAS_PART -> claimValue instanceof List claimList && claimList.contains(rightValue); + default -> false; + }; + } + + @Override + public boolean canHandle(Object leftValue) { + return leftValue instanceof String leftValueString && BASIC_ABAC_PATTERN.matcher(leftValueString).matches(); + } + + private boolean handleRightValueList(Object leftValue, Operator operator, List rightValueList, Permission rule, C context) { + Object claimValue = extractValueFromCredentialSubject(context, leftValue); + if (claimValue == null) return false; + return switch (operator) { + case IN -> { + if (claimValue instanceof List claimList) { + yield !claimList.isEmpty() && rightValueList.containsAll(claimList); + } + yield rightValueList.contains(claimValue); + } + case IS_ANY_OF -> { + if (claimValue instanceof List claimList) { + yield !Collections.disjoint(claimList, rightValueList); + } + yield rightValueList.contains(claimValue); + } + + case IS_ALL_OF -> { + if (rightValueList.isEmpty()) { + yield true; + } + if (claimValue instanceof List claimList) { + yield claimList.containsAll(rightValueList); + } + yield rightValueList.size() == 1 + && rightValueList.contains(claimValue); + } + + case IS_NONE_OF -> { + if (claimValue instanceof List claimList) { + yield Collections.disjoint(claimList, rightValueList); + } + yield !rightValueList.contains(claimValue); + } + + case HAS_PART -> { + if (claimValue instanceof List claimList) { + yield claimList.containsAll(rightValueList); + } + yield rightValueList.size() == 1 + && rightValueList.contains(claimValue); + } + + case EQ -> claimValue instanceof List claimList && listsEqualAsSets(claimList, rightValueList); + + case NEQ -> !(claimValue instanceof List claimList) || !listsEqualAsSets(claimList, rightValueList); + + default -> false; + }; + } + + private boolean listsEqualAsSets(List left, List right) { + return left.containsAll(right) && right.containsAll(left); + } + + private boolean handleRightValueNumeric(Object leftValue, Operator operator, double expectedNumber, Permission rule, C context) { + Object valueFromCredentialClaims = extractValueFromCredentialSubject(context, leftValue); + try { + if (valueFromCredentialClaims instanceof List claimList && operator.equals(Operator.HAS_PART)) { + return normalizeNumericToDoubleOrBoolean(claimList).contains(expectedNumber); + } + + double numericFromCredentialClaims = Double.parseDouble(valueFromCredentialClaims.toString()); + return switch (operator) { + case EQ -> numericFromCredentialClaims == expectedNumber; + case NEQ -> numericFromCredentialClaims != expectedNumber; + case GEQ -> numericFromCredentialClaims >= expectedNumber; + case LEQ -> numericFromCredentialClaims <= expectedNumber; + case GT -> numericFromCredentialClaims > expectedNumber; + case LT -> numericFromCredentialClaims < expectedNumber; + default -> false; + }; + } catch (Exception e) { + return false; + } + } + + private @Nullable Object extractValueFromCredentialSubject(C context, Object leftValue) { + var verifiableCredentialList = getVerifiableCredentialList(context); + if (verifiableCredentialList == null) return null; + String requiredCredentialType = truncateLastPathSegment(leftValue); + for (var credential : verifiableCredentialList) { + if (credential.getType() == null || !credential.getType().contains(requiredCredentialType)) { + continue; + } + for (var credentialSubject : credential.getCredentialSubject()) { + Object value = getPathObject(leftValue, credentialSubject.getClaims()); + if (value != null) { + return value; + } + } + } + return null; + } + + private static @Nullable List getVerifiableCredentialList(C context) { + List verifiableCredentialList = null; + String potentialProblem = "No Credential Claims found"; + try { + var participantAgent = context.participantAgent(); + var supposedToBeCredentialList = participantAgent.getClaims().get("vc"); + if (supposedToBeCredentialList instanceof List credentialList) { + if (credentialList.stream().allMatch(it -> it instanceof VerifiableCredential)) { + verifiableCredentialList = (List) credentialList; + } + } + } catch (Exception e) { + context.reportProblem(potentialProblem); + return null; + } + if (verifiableCredentialList == null || verifiableCredentialList.isEmpty()) { + context.reportProblem(potentialProblem); + return null; + } + return verifiableCredentialList; + } + +} diff --git a/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacExtension.java b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacExtension.java new file mode 100644 index 0000000000..4302cea8d1 --- /dev/null +++ b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacExtension.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.fraunhofer.isst.edc.extension.basic_abac.dev; + +import org.eclipse.edc.connector.controlplane.catalog.spi.policy.CatalogPolicyContext; +import org.eclipse.edc.connector.controlplane.contract.spi.policy.ContractNegotiationPolicyContext; +import org.eclipse.edc.connector.controlplane.contract.spi.policy.TransferProcessPolicyContext; +import org.eclipse.edc.policy.context.request.spi.RequestCatalogPolicyContext; +import org.eclipse.edc.policy.context.request.spi.RequestContractNegotiationPolicyContext; +import org.eclipse.edc.policy.context.request.spi.RequestTransferProcessPolicyContext; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.util.Set; + +import static de.fraunhofer.isst.edc.extension.basic_abac.dev.BasicAbacUtils.*; +import static org.eclipse.edc.connector.controlplane.catalog.spi.policy.CatalogPolicyContext.CATALOG_SCOPE; +import static org.eclipse.edc.connector.controlplane.contract.spi.policy.ContractNegotiationPolicyContext.NEGOTIATION_SCOPE; +import static org.eclipse.edc.connector.controlplane.contract.spi.policy.TransferProcessPolicyContext.TRANSFER_SCOPE; +import static org.eclipse.edc.policy.context.request.spi.RequestCatalogPolicyContext.CATALOGING_REQUEST_SCOPE; +import static org.eclipse.edc.policy.context.request.spi.RequestContractNegotiationPolicyContext.CONTRACT_NEGOTIATION_REQUEST_SCOPE; +import static org.eclipse.edc.policy.context.request.spi.RequestTransferProcessPolicyContext.TRANSFER_PROCESS_REQUEST_SCOPE; +import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; + + +@Extension("Basic Abac Extension") +public class BasicAbacExtension implements ServiceExtension { + + @Setting(description = "The default credential type to be used", + defaultValue = "https://w3id.org/constructx/credentials/v1.0/ConstructXMembershipCredential", + key = "edc.abac.defaultcredential") + private String defaultCredential; + + @Inject + private PolicyEngine policyEngine; + + @Inject + private RuleBindingRegistry ruleBindingRegistry; + + + @Override + public String name() { + return "Basic Abac Extension"; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + + BasicAbacUtils.DEFAULT_MEMBERSHIP_SCOPE = DCP_PREFIX + defaultCredential + READ_SUFFIX; + monitor.withPrefix(this.getClass().getSimpleName()) + .info("Using default as default scope: " + BasicAbacUtils.DEFAULT_MEMBERSHIP_SCOPE); + + policyEngine.registerPostValidator(RequestCatalogPolicyContext.class, new BasicAbacPolicyPostValidator<>(monitor)); + policyEngine.registerPostValidator(RequestContractNegotiationPolicyContext.class, new BasicAbacPolicyPostValidator<>(monitor)); + policyEngine.registerPostValidator(RequestTransferProcessPolicyContext.class, new BasicAbacPolicyPostValidator<>(monitor)); + + for (var clazz : new Class[]{ + CatalogPolicyContext.class, + ContractNegotiationPolicyContext.class, + TransferProcessPolicyContext.class}) { + + policyEngine.registerFunction(clazz, Permission.class, new BasicAbacCredentialConstraintFunction<>()); + } + + ruleBindingRegistry.dynamicBind(str -> { + if (BASIC_ABAC_PATTERN.matcher(str).matches()) { + return Set.of( + CATALOGING_REQUEST_SCOPE, + CONTRACT_NEGOTIATION_REQUEST_SCOPE, + TRANSFER_PROCESS_REQUEST_SCOPE, + CATALOG_SCOPE, + NEGOTIATION_SCOPE, + TRANSFER_SCOPE + ); + } + return Set.of(); + }); + + String ODRL_USE = ODRL_SCHEMA + "use"; + ruleBindingRegistry.bind(ODRL_USE, CATALOG_SCOPE); + ruleBindingRegistry.bind(ODRL_USE, NEGOTIATION_SCOPE); + ruleBindingRegistry.bind(ODRL_USE, TRANSFER_SCOPE); + } +} diff --git a/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacPolicyPostValidator.java b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacPolicyPostValidator.java new file mode 100644 index 0000000000..9485da87c2 --- /dev/null +++ b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacPolicyPostValidator.java @@ -0,0 +1,70 @@ +package de.fraunhofer.isst.edc.extension.basic_abac.dev; + +import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; +import org.eclipse.edc.policy.engine.spi.PolicyValidatorRule; +import org.eclipse.edc.policy.model.*; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.util.HashSet; +import java.util.Set; + +import static de.fraunhofer.isst.edc.extension.basic_abac.dev.BasicAbacUtils.*; + +public class BasicAbacPolicyPostValidator implements PolicyValidatorRule { + private final Monitor monitor; + + public BasicAbacPolicyPostValidator(Monitor monitor) { + this.monitor = monitor.withPrefix(this.getClass().getSimpleName()); + } + + + @Override + public Boolean apply(Policy policy, C requestPolicyContext) { + var foundAbacCredentialTypeIdentifiers = explorePolicy(policy); + requestPolicyContext.requestScopeBuilder().scopes(foundAbacCredentialTypeIdentifiers); + monitor.debug("Found credential type identifiers " + foundAbacCredentialTypeIdentifiers); + + // Minimally require the default credential + requestPolicyContext.requestScopeBuilder().scope(DEFAULT_MEMBERSHIP_SCOPE); + return true; + } + + private Set explorePolicy(Policy policy) { + Set output = new HashSet<>(); + for (var permission : policy.getPermissions()) { + for (var constraint : permission.getConstraints()) { + output.addAll(exploreConstraint(constraint)); + } + } + return output; + } + + private Set exploreConstraint(Constraint constraint) { + Set output = new HashSet<>(); + if (constraint instanceof MultiplicityConstraint multiplicityConstraint) { + for (var nestedConstraint : multiplicityConstraint.getConstraints()) { + output.addAll(exploreConstraint(nestedConstraint)); + } + } else if (constraint instanceof AtomicConstraint atomicConstraint) { + if (atomicConstraint.getLeftExpression() instanceof LiteralExpression literalExpression) { + if (isCredentialConstraint(literalExpression.getValue())) { + output.add(DCP_PREFIX + truncateLastPathSegment(literalExpression.getValue()) + READ_SUFFIX); + } + } + } + return output; + } + + private boolean isCredentialConstraint(Object leftExpression) { + if (leftExpression instanceof String leftExpressionString) { + return BASIC_ABAC_PATTERN.matcher(leftExpressionString).matches(); + } + return false; + } + + + @Override + public String name() { + return this.getClass().getName() + "-Rule"; + } +} diff --git a/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacUtils.java b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacUtils.java new file mode 100644 index 0000000000..1e06b7326b --- /dev/null +++ b/edc-extensions/basic-abac/src/main/java/de/fraunhofer/isst/edc/extension/basic_abac/dev/BasicAbacUtils.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.fraunhofer.isst.edc.extension.basic_abac.dev; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class BasicAbacUtils { + + public static final String BASIC_ABAC_REGEX = + "^https://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}(?::[0-9]+)?(?:/[^\\s/]+)*/[A-Z][^\\s/.]*\\.credentialSubject(?:\\.[^\\s/.]+)+$"; + public static final Pattern BASIC_ABAC_PATTERN = Pattern.compile(BASIC_ABAC_REGEX); + + public static final String DCP_PREFIX = "org.eclipse.dspace.dcp.vc.type:"; + public static final String READ_SUFFIX = ":read"; + static String DEFAULT_MEMBERSHIP_SCOPE; // get initialized by the extension class + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * This method takes as input + *

+ * - the leftValue Parameter of the evaluate method from the DynamicAtomicConstraintRuleFunction interface. + *

+ * - the credential subject of a verifiable credential (as processed by the edc framework, i.e. as a java.util.Map) + *

+ * The leftValue is expected to have matched the BASIC_ABAC_REGEX, containing a JSONPath at the end. + * That suffix will be interpreted by this method to navigate through the credential subject (also see the readme). + * If the found value is numeric, then -for the sake of normalization- it will be converted to a Double value. + * + * @param leftValue + * @param map + * @return the object (which might be a string, a list, a map or a Double) or null if nothing could be found + */ + public static Object getPathObject(Object leftValue, Map map) { + String jsonPath = leftValue.toString().replace(truncateLastPathSegment(leftValue) + ".credentialSubject.", ""); + if (jsonPath.isBlank()) return null; + Object current = map; + String[] segments = jsonPath.split("\\."); + for (int i = 0; i < segments.length; i++) { + String fieldName = segments[i].strip(); + Integer arrayIndex = null; + int bracketStart = fieldName.indexOf("["); + int bracketEnd = fieldName.indexOf("]"); + if (bracketStart >= 0 && bracketEnd > bracketStart) { + String indexString = fieldName.substring(bracketStart + 1, bracketEnd); + fieldName = fieldName.substring(0, bracketStart); + arrayIndex = Integer.parseInt(indexString); + } + if (!fieldName.isBlank()) { + if (current instanceof Map nestedMap) { + current = nestedMap.get(fieldName); + } else { + return null; + } + } + if (current == null) { + return null; + } + if (arrayIndex != null) { + if (current instanceof List nestedList && arrayIndex >= 0 && arrayIndex < nestedList.size()) { + try { + current = nestedList.get(arrayIndex); + } catch (Exception e) { + return null; + } + } else { + return null; + } + } + } + if (current instanceof List foundList) { + return normalizeNumericToDoubleOrBoolean(foundList); + } + + try { + return Double.parseDouble(current.toString()); + } catch (Exception e) { + return normalizeToBoolean(current); + } + + } + + /** + * This method expects the leftValue Parameter of the evaluate method from the DynamicAtomicConstraintRuleFunction + * interface as input. It is also expected that the left expression was matched by the BASIC_ABAC_REGEX. + *

+ * It will remove the JSONPath, effectively returning the fully qualified name of the credential. + * + * @param leftExpression + * @return the fully qualified name of the credential or null if the leftExpression was invalid + */ + public static String truncateLastPathSegment(Object leftExpression) { + if (leftExpression instanceof String url) { + int lastSlashIndex = url.lastIndexOf('/'); + if (lastSlashIndex == -1) { + return null; + } + String beforeLastSegment = url.substring(0, lastSlashIndex + 1); + String lastSegment = url.substring(lastSlashIndex + 1); + int firstDotIndex = lastSegment.indexOf('.'); + if (firstDotIndex == -1) { + return null; + } + return beforeLastSegment + lastSegment.substring(0, firstDotIndex); + } + return null; + } + + /** + * Convert the string representation of a JSON list into a Java list. + * + * @param jsonString the string representation of a JSON list + * @return a (Java) list of objects (of unspecified types) + */ + public static @Nullable List convertJsonToList(String jsonString) { + try { + var parsedJson = MAPPER.readTree(jsonString); + if (parsedJson.isArray()) { + return (List) convert(parsedJson); + } + return null; + } catch (Exception e) { + return null; + } + } + + private static Object convert(JsonNode node) { + if (node == null || node.isNull()) { + return null; + } + if (node.isObject()) { + return convertObject((ObjectNode) node); + } + if (node.isArray()) { + return convertArray((ArrayNode) node); + } + if (node.isBoolean()) { + return node.asBoolean(); + } + if (node.isNumber()) { + return node.asDouble(); + } + return node.asText(); + } + + private static Map convertObject(ObjectNode objNode) { + Map map = new LinkedHashMap<>(); + objNode.propertyStream().forEach(entry -> { + map.put(entry.getKey(), convert(entry.getValue())); + }); + return map; + } + + private static List convertArray(ArrayNode arrNode) { + List list = new ArrayList<>(); + for (JsonNode child : arrNode) { + list.add(convert(child)); + } + return list; + } + + /** + * Returns a copy of the input list, where contained objects are parsed into + * Double values, if possible. I.e. Integer, Long or String values + * (with numeric content) will be converted into Doubles. + * + * @param list + * @return a new list + */ + public static List normalizeNumericToDoubleOrBoolean(List list) { + return list.stream() + .map(it -> { + if (it instanceof List nestedList) { + return normalizeNumericToDoubleOrBoolean(nestedList); + } + // Note: Nested maps unsupported, doubtful if it's needed + try { + return Double.parseDouble(it.toString()); + } catch (NumberFormatException e) { + return normalizeToBoolean(it); + } + }).toList(); + } + + public static @Nullable Object normalizeToBoolean(@Nullable Object value) { + if (value != null && ("true".equalsIgnoreCase(value.toString()) || "false".equalsIgnoreCase(value.toString()))) { + return Boolean.valueOf(value.toString()); + } + return value; + + } +} diff --git a/edc-extensions/basic-abac/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/basic-abac/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000000..6dcb9a0e46 --- /dev/null +++ b/edc-extensions/basic-abac/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,20 @@ +################################################################################# +# Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +de.fraunhofer.isst.edc.extension.basic_abac.dev.BasicAbacExtension \ No newline at end of file diff --git a/edc-extensions/basic-abac/src/test/java/BasicAbacTest.java b/edc-extensions/basic-abac/src/test/java/BasicAbacTest.java new file mode 100644 index 0000000000..5ee1714218 --- /dev/null +++ b/edc-extensions/basic-abac/src/test/java/BasicAbacTest.java @@ -0,0 +1,684 @@ +/* + * Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * + * 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. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import de.fraunhofer.isst.edc.extension.basic_abac.dev.BasicAbacUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class BasicAbacTest { + + // Tests for BasicAbacUtils.BASIC_ABAC_PATTERN + + @ParameterizedTest + @ValueSource(strings = { + "https://w3id.org/constructx/credentials/v1.0/Foo.credentialSubject.fooLevel", + "https://w3id.org/constructx/notcredentials/v1.0/Bar.credentialSubject.nestedObject.barLevel", + "https://my-domain.com:8080/some/fancy/path/segments/Foo.credentialSubject.fooLevel", + + "https://w3id.org/constructx/policies/v1.0/Foo.credentialSubject.fooLevel", + "https://example.org/Foo.credentialSubject.fooLevel", + + // Multiple JSONPath segments + "https://example.org/types/EmployeeCredential.credentialSubject.address.city", + "https://example.org/types/Employee123Credential.credentialSubject.department.name" + }) + void shouldMatchValidBasicAbacOperands(String leftOperand) { + assertTrue( + BasicAbacUtils.BASIC_ABAC_PATTERN.matcher(leftOperand).matches(), + () -> "Expected leftOperand to match regex: " + leftOperand + ); + } + + @ParameterizedTest + @ValueSource(strings = { + // Only HTTPS is allowed + "http://example.org/credentials/Foo.credentialSubject.fooLevel", + + // The credential type must start with an uppercase letter + "https://example.org/credentials/foo.credentialSubject.fooLevel", + + // .credentialSubject is missing + "https://example.org/credentials/Foo.fooLevel", + + // JSONPath segment after .credentialSubject is missing + "https://example.org/credentials/Foo.credentialSubject", + + // Empty JSONPath segment + "https://example.org/credentials/Foo.credentialSubject..fooLevel", + + // Incorrect spelling or capitalization of credentialSubject + "https://example.org/credentials/Foo.credentialsSubject.fooLevel", + "https://example.org/credentials/Foo.CredentialSubject.fooLevel", + + // .credentialSubject is not directly after the credential type + "https://example.org/credentials/Foo.additional.credentialSubject.fooLevel", + + // Trailing dot or slash + "https://example.org/credentials/Foo.credentialSubject.", + "https://example.org/credentials/Foo.credentialSubject.fooLevel/", + + // Missing host or top-level domain + "https://localhost/Foo.credentialSubject.fooLevel", + "https:///Foo.credentialSubject.fooLevel", + + // Not a complete HTTPS URL + "Foo.credentialSubject.fooLevel", + "" + }) + void shouldNotMatchInvalidBasicAbacOperands(String leftOperand) { + assertFalse( + BasicAbacUtils.BASIC_ABAC_PATTERN.matcher(leftOperand).matches(), + () -> "Expected leftOperand not to match regex: " + leftOperand + ); + } + + + // Tests for BasicAbacUtils.truncateLastPathSegment + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "https://w3id.org/constructx/credentials/v1.0/Foo.credentialSubject.fooLevel" + + "|https://w3id.org/constructx/credentials/v1.0/Foo", + "https://w3id.org/constructx/credentials/v1.0/Bar.credentialSubject.nestedObject.barLevel" + + "|https://w3id.org/constructx/credentials/v1.0/Bar", + "https://my-domain.com:8080/credentials/Foo.credentialSubject.fooLevel" + + "|https://my-domain.com:8080/credentials/Foo", + "https://w3id.org/constructx/policies/v1.0/Employee123Credential.credentialSubject.department.name" + + "|https://w3id.org/constructx/policies/v1.0/Employee123Credential", + "https://example.org/Foo.credentialSubject.fooLevel" + + "|https://example.org/Foo" + } + ) + void shouldRemoveJsonPathFromLeftExpression(String leftExpression, String expectedCredentialType) { + String result = BasicAbacUtils.truncateLastPathSegment(leftExpression); + + assertEquals(expectedCredentialType, result); + } + + @Test + void shouldNotRemoveDotsFromPreviousPathSegments() { + String leftExpression = + "https://example.org/path.with.dots/v1.0/Foo.credentialSubject.value"; + + String result = BasicAbacUtils.truncateLastPathSegment(leftExpression); + + assertEquals( + "https://example.org/path.with.dots/v1.0/Foo", + result + ); + } + + @ParameterizedTest + @ValueSource(strings = { + // The last path segment does not contain a dot + "https://example.org/credentials/Foo", + + // The expression does not contain a slash + "Foo.credentialSubject.fooLevel", + + // The last path segment is empty + "https://example.org/credentials/", + + // Empty expression + "" + }) + void shouldReturnNullForInvalidStringExpression(String leftExpression) { + String result = BasicAbacUtils.truncateLastPathSegment(leftExpression); + + assertNull(result); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 42 }) + void shouldReturnNullNonStringExpression(Object leftExpression) { + String result = BasicAbacUtils.truncateLastPathSegment(leftExpression); + + assertNull(result); + } + + + // Tests for BasicAbacUtils.getPathObject + + @Test + void shouldReturnStringFromCredentialSubject() { + Map credentialSubject = Map.of( + "firstName", "Alice" + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.firstName", + credentialSubject + ); + + assertEquals("Alice", result); + } + + @Test + void shouldNavigateThroughNestedObjects() { + Map credentialSubject = Map.of( + "address", Map.of( + "city", "Berlin", + "country", "Germany" + ) + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.address.city", + credentialSubject + ); + + assertEquals("Berlin", result); + } + + @Test + void shouldNormalizeIntegerToDouble() { + Map credentialSubject = Map.of( + "accessLevel", 4 + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.accessLevel", + credentialSubject + ); + + assertEquals(4.0, result); + } + + @Test + void shouldReturnDoubleValue() { + Map credentialSubject = Map.of( + "score", 12.5 + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.score", + credentialSubject + ); + + assertEquals(12.5, result); + } + + @Test + void shouldNormalizeNumericStringToDouble() { + Map credentialSubject = Map.of( + "accessLevel", "4" + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.accessLevel", + credentialSubject + ); + + assertEquals(4.0, result); + } + + @Test + void shouldReturnBooleanValue() { + Map credentialSubject = Map.of( + "active", true + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.active", + credentialSubject + ); + + assertEquals(true, result); + } + + @Test + void shouldReturnNestedMap() { + Map address = new LinkedHashMap<>(); + address.put("city", "Berlin"); + address.put("postalCode", "10115"); + + Map credentialSubject = Map.of( + "address", address + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.address", + credentialSubject + ); + + assertSame(address, result); + } + + @Test + void shouldReturnAndNormalizeList() { + Map credentialSubject = Map.of( + "values", List.of(1, 2.5, "3", true, "text") + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.values", + credentialSubject + ); + + assertEquals( + List.of(1.0, 2.5, 3.0, true, "text"), + result + ); + } + + @Test + void shouldAccessListElementByIndex() { + Map credentialSubject = Map.of( + "roles", List.of("reader", "editor", "admin") + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.roles[1]", + credentialSubject + ); + + assertEquals("editor", result); + } + + @Test + void shouldNavigateThroughObjectInsideList() { + Map credentialSubject = Map.of( + "addresses", List.of( + Map.of("city", "Berlin"), + Map.of("city", "Munich") + ) + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.addresses[1].city", + credentialSubject + ); + + assertEquals("Munich", result); + } + + @Test + void shouldNavigateThroughMultipleNestedListsAndObjects() { + Map credentialSubject = Map.of( + "departments", List.of( + Map.of( + "name", "Engineering", + "employees", List.of( + Map.of("name", "Alice", "accessLevel", 3), + Map.of("name", "Bob", "accessLevel", 4) + ) + ) + ) + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential" + + ".credentialSubject.departments[0].employees[1].accessLevel", + credentialSubject + ); + + assertEquals(4.0, result); + } + + @Test + void shouldReturnNullWhenFieldDoesNotExist() { + Map credentialSubject = Map.of( + "firstName", "Alice" + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.lastName", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullWhenNestedFieldDoesNotExist() { + Map credentialSubject = Map.of( + "address", Map.of( + "city", "Berlin" + ) + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.address.country", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullWhenListIndexIsOutOfBounds() { + Map credentialSubject = Map.of( + "roles", List.of("reader", "editor") + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.roles[5]", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullWhenListIndexIsNegative() { + Map credentialSubject = Map.of( + "roles", List.of("reader", "editor") + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.roles[-1]", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullWhenIndexedValueIsNotAList() { + Map credentialSubject = Map.of( + "role", "admin" + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.role[0]", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullWhenIntermediateValueIsNotAMap() { + Map credentialSubject = Map.of( + "address", "Berlin" + ); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.address.city", + credentialSubject + ); + + assertNull(result); + } + + @Test + void shouldReturnNullForEmptyCredentialSubject() { + Map credentialSubject = Map.of(); + + Object result = BasicAbacUtils.getPathObject( + "https://example.org/EmployeeCredential.credentialSubject.firstName", + credentialSubject + ); + + assertNull(result); + } + + + + + // Tests for BasicAbacUtils.convertJsonToList + + @Test + void shouldConvertEmptyJsonArrayToEmptyList() { + List result = BasicAbacUtils.convertJsonToList("[]"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldConvertJsonArrayContainingStrings() { + List result = BasicAbacUtils.convertJsonToList( + "[\"reader\", \"editor\", \"admin\"]" + ); + + assertEquals( + List.of("reader", "editor", "admin"), + result + ); + } + + @Test + void shouldConvertJsonNumbersToDoubles() { + List result = BasicAbacUtils.convertJsonToList( + "[1, 2.5, -3, 0]" + ); + + assertEquals( + List.of(1.0, 2.5, -3.0, 0.0), + result + ); + } + + @Test + void shouldConvertJsonBooleans() { + List result = BasicAbacUtils.convertJsonToList( + "[true, false]" + ); + + assertEquals( + List.of(true, false), + result + ); + } + + @Test + void shouldPreserveJsonNullValues() { + List expected = new ArrayList<>(); + expected.add("first"); + expected.add(null); + expected.add("third"); + + List result = BasicAbacUtils.convertJsonToList( + "[\"first\", null, \"third\"]" + ); + + assertEquals(expected, result); + } + + @Test + void shouldConvertArrayContainingDifferentJsonTypes() { + List expected = new ArrayList<>(); + expected.add("text"); + expected.add(42.0); + expected.add(true); + expected.add(null); + + List result = BasicAbacUtils.convertJsonToList( + "[\"text\", 42, true, null]" + ); + + assertEquals(expected, result); + } + + @Test + void shouldConvertNestedJsonArrays() { + List result = BasicAbacUtils.convertJsonToList( + "[[1, 2], [3, 4], []]" + ); + + assertEquals( + List.of( + List.of(1.0, 2.0), + List.of(3.0, 4.0), + List.of() + ), + result + ); + } + + @Test + void shouldConvertJsonObjectsInsideArrayToMaps() { + List result = BasicAbacUtils.convertJsonToList( + """ + [ + { + "name": "Alice", + "accessLevel": 4, + "active": true + }, + { + "name": "Bob", + "accessLevel": 3, + "active": false + } + ] + """ + ); + + assertEquals( + List.of( + Map.of( + "name", "Alice", + "accessLevel", 4.0, + "active", true + ), + Map.of( + "name", "Bob", + "accessLevel", 3.0, + "active", false + ) + ), + result + ); + } + + @Test + void shouldConvertNestedObjectsAndArrays() { + String json = """ + [ + { + "department": { + "name": "Engineering", + "employees": [ + { + "name": "Alice", + "accessLevel": 3 + }, + { + "name": "Bob", + "accessLevel": 4 + } + ] + } + } + ] + """; + + Map alice = new LinkedHashMap<>(); + alice.put("name", "Alice"); + alice.put("accessLevel", 3.0); + + Map bob = new LinkedHashMap<>(); + bob.put("name", "Bob"); + bob.put("accessLevel", 4.0); + + Map department = new LinkedHashMap<>(); + department.put("name", "Engineering"); + department.put("employees", List.of(alice, bob)); + + Map rootObject = new LinkedHashMap<>(); + rootObject.put("department", department); + + List result = BasicAbacUtils.convertJsonToList(json); + + assertEquals(List.of(rootObject), result); + } + + @Test + void shouldPreserveNullValuesInsideJsonObjects() { + String json = """ + [ + { + "name": "Alice", + "optionalValue": null + } + ] + """; + + Map expectedObject = new LinkedHashMap<>(); + expectedObject.put("name", "Alice"); + expectedObject.put("optionalValue", null); + + List result = BasicAbacUtils.convertJsonToList(json); + + assertEquals(List.of(expectedObject), result); + } + + @Test + void shouldConvertEscapedAndUnicodeStrings() { + List result = BasicAbacUtils.convertJsonToList( + "[\"Hello\\\\World\", \"Line 1\\nLine 2\", \"München\"]" + ); + + assertEquals( + List.of( + "Hello\\World", + "Line 1\nLine 2", + "München" + ), + result + ); + } + + @ParameterizedTest + @ValueSource(strings = { + // A JSON object is not a list + "{}", + "{\"name\":\"Alice\"}", + + // Primitive JSON values are not lists + "\"text\"", + "42", + "true", + "null", + + // Empty or malformed JSON + "", + "not-json", + "[1, 2", + "[1,,2]", + "{invalid}" + }) + void shouldReturnNullWhenInputIsNotAValidJsonArray(String jsonString) { + List result = BasicAbacUtils.convertJsonToList(jsonString); + + assertNull(result); + } + + @ParameterizedTest + @ValueSource(strings = { + " ", + "\t", + "\n" + }) + void shouldReturnNullForNullOrBlankInput(String jsonString) { + List result = BasicAbacUtils.convertJsonToList(jsonString); + + assertNull(result); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fa7c1e00e8..9e8f5bb070 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,6 +110,7 @@ include(":edc-extensions:agreements:retirement-evaluation-api") include(":edc-extensions:agreements:retirement-evaluation-spi") include(":edc-extensions:agreements:retirement-evaluation-store-sql") include(":edc-extensions:agreements:retirement-evaluation-bootstrapping") +include(":edc-extensions:basic-abac") include(":edc-extensions:dynamic-issuers")