diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml new file mode 100644 index 0000000..f093bed --- /dev/null +++ b/.github/workflows/e2e-tests.yaml @@ -0,0 +1,112 @@ +# +# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft +# +# 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 +# + +--- +name: "Run E2E Tests" + +on: + push: + pull_request: + + workflow_run: + workflows: [ "Draft Release" ] + types: + - completed + + schedule: + - cron: '0 3 * * *' # Run at 3am UTC every day + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + E2E-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: eclipse-edc/.github/.github/actions/setup-build@main + + - name: "Setup Kubectl" + uses: azure/setup-kubectl@v4 + + - name: "Build runtime images" + run: | + ./gradlew dockerize + docker buildx build -f launchers/postgres/Dockerfile -t ghcr.io/metaform/jad/postgres:wal2json launchers/postgres + + - name: "Create k8s Kind Cluster" + uses: helm/kind-action@v1.12.0 + with: + config: kind.config.yaml + cluster_name: jad + + - name: "Load runtime images into KinD" + run: | + kind load docker-image -n jad ghcr.io/metaform/jad/postgres:wal2json \ + ghcr.io/metaform/jad/controlplane:latest \ + ghcr.io/metaform/jad/dataplane:latest \ + ghcr.io/metaform/jad/identity-hub:latest \ + ghcr.io/metaform/jad/issuerservice:latest \ + + + - name: "Install nginx ingress controller" + run: |- + kubectl apply -f https://kind.sigs.k8s.io/examples/ingress/deploy-ingress-nginx.yaml + kubectl wait --namespace ingress-nginx \ + --for=condition=ready pod \ + --selector=app.kubernetes.io/component=controller \ + --timeout=90s + + - name: "Deploy JAD infrastructure" + run: | + # deploying the infrastructure first, wait for it to finish and deploying the applications afterwards is + # the safest way to avoid race conditions between apps and infra, e.g. vault. + + kubectl apply -f k8s/base + kubectl wait --for=condition=Ready pods --all -n edc-v --timeout=90s + + - name: "Deploy JAD applications" + run: |- + # this is crucial - without it, KinD would take the prebuilt images from GHCR + grep -rlZ "imagePullPolicy:.*Always" . | xargs sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" + + kubectl apply -f k8s/apps + kubectl wait --namespace edc-v \ + --for=condition=ready pod \ + --selector=type=edcv-app \ + --timeout=90s + + - name: "Run E2E Test" + run: | + ./gradlew test -DincludeTags="EndToEndTest" + + - name: "Print log if test failed" + if: failure() + run: |- + kubectl logs deployment/controlplane -n edc-v + kubectl logs deployment/dataplane -n edc-v + + - name: "Destroy the KinD cluster" + run: >- + kind delete cluster -n jad diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 2ff8da9..7229644 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -79,13 +79,3 @@ jobs: run: | ./gradlew shadowJar ./gradlew test -DincludeTags="PostgresqlIntegrationTest" - - e2e-Tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: eclipse-edc/.github/.github/actions/setup-build@main - - name: End to End Integration Tests - run: | - ./gradlew shadowJar - ./gradlew test -DincludeTags="EndToEndTest" diff --git a/README.md b/README.md index a850b66..7498dcd 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ Create another environment to suit your setup: ### Update deployment manifests -in [keycloak.yaml](k8s/keycloak.yaml) and [vault.yaml](k8s/vault.yaml), update the `host` fields in the `Ingress` +in [keycloak.yaml](k8s/base/keycloak.yaml) and [vault.yaml](k8s/base/vault.yaml), update the `host` fields in the `Ingress` resources to match your DNS: ```yaml @@ -204,7 +204,7 @@ spec: http: ``` -Next, in the [controlplane-config.yaml](./k8s/controlplane-config.yaml) change the expected issuer URL to match your +Next, in the [controlplane-config.yaml](k8s/apps/controlplane-config.yaml) change the expected issuer URL to match your DNS: ```yaml edc.iam.oauth2.issuer: "http://keycloak.localhost/realms/edcv" # change to "http://auth.yourdomain.com/realms/edcv" diff --git a/build.gradle.kts b/build.gradle.kts index ffc5932..c502984 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ subprojects { val dockerContextDir = project.projectDir dockerFile.set(file("$dockerContextDir/src/main/docker/Dockerfile")) images.add("ghcr.io/metaform/jad/${project.name}:${project.version}") + images.add("ghcr.io/metaform/jad/${project.name}:latest") //images.add("${project.name}:latest") // specify platform with the -Dplatform flag: diff --git a/dependabot.yml b/dependabot.yml deleted file mode 100644 index c7ef91d..0000000 --- a/dependabot.yml +++ /dev/null @@ -1,92 +0,0 @@ -# -# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft -# -# 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 -# - ---- -version: 2 -updates: - # maintain dependencies for GitHub actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" # default = monday - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "github_actions" - - # maintain dependencies for Gradle - - package-ecosystem: "gradle" # checks build.gradle(.kts) and settings.gradle(.kts) - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "java" - ignore: - - dependency-name: "org.eclipse.edc:edc-versions" - - - package-ecosystem: "terraform" - directory: "/deployment" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "terraform" - - # Docker catalog-server - - package-ecosystem: "docker" - target-branch: main - directory: "./launchers/catalog-server/src/main/docker/" - labels: - - "dependabot" - - "docker" - schedule: - interval: "weekly" - - # Docker CP - - package-ecosystem: "docker" - target-branch: main - directory: "./launchers/controlplane/src/main/docker/" - labels: - - "dependabot" - - "docker" - schedule: - interval: "weekly" - - # Docker DP - - package-ecosystem: "docker" - target-branch: main - directory: "./launchers/dataplane/src/main/docker/" - labels: - - "dependabot" - - "docker" - schedule: - interval: "weekly" - - # Docker IH - - package-ecosystem: "docker" - target-branch: main - directory: "./launchers/identity-hub/src/main/docker/" - labels: - - "dependabot" - - "docker" - schedule: - interval: "weekly" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5b9baa..c7d76e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,12 @@ format.version = "1.1" [versions] +awaitility = "4.3.0" edc = "0.15.0-SNAPSHOT" edc-build = "1.1.4" +jackson = "2.20.1" +jackson-annotations = "2.20" +restAssured = "5.5.6" bouncyCastle-jdk18on = "1.83" [libraries] @@ -20,6 +24,7 @@ edc-core-participantcontext-config = { module = "org.eclipse.edc:participant-con edc-core-edrstore = { module = "org.eclipse.edc:edr-store-core", version.ref = "edc" } edc-dcp = { module = "org.eclipse.edc:decentralized-claims-service", version.ref = "edc" } edc-api-observability = { module = "org.eclipse.edc:api-observability", version.ref = "edc" } +edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } edc-dataplane-v2 = { module = "org.eclipse.edc:data-plane-public-api-v2", version.ref = "edc" } edc-core-dataplane-selector = { module = "org.eclipse.edc:data-plane-selector-core", version.ref = "edc" } edc-core-dataplane-signaling-client = { module = "org.eclipse.edc:data-plane-signaling-client", version.ref = "edc" } @@ -64,6 +69,7 @@ edc-spi-edrstore = { module = "org.eclipse.edc:edr-store-spi", version.ref = "ed # identityhub SPI modules edc-ih-spi-credentials = { module = "org.eclipse.edc:verifiable-credential-spi", version.ref = "edc" } +edc-ih-spi-participantcontext = { module = "org.eclipse.edc:participant-context-spi", version.ref = "edc" } edc-ih-spi = { module = "org.eclipse.edc:identity-hub-spi", version.ref = "edc" } # identityhub API modules @@ -84,9 +90,13 @@ edc-bom-issuerservice = { module = "org.eclipse.edc:issuerservice-bom", version. edc-bom-issuerservice-sql = { module = "org.eclipse.edc:issuerservice-feature-sql-bom", version.ref = "edc" } # Third party deps +awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } bouncyCastle-bcprovJdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle-jdk18on" } +jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson-annotations" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } tink = { module = "com.google.crypto.tink:tink", version = "1.19.0" } nats-client = { module = "io.nats:jnats", version = "2.24.1" } +restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } [bundles] dcp = [ diff --git a/k8s/controlplane-config.yaml b/k8s/apps/controlplane-config.yaml similarity index 100% rename from k8s/controlplane-config.yaml rename to k8s/apps/controlplane-config.yaml diff --git a/k8s/controlplane.yaml b/k8s/apps/controlplane.yaml similarity index 99% rename from k8s/controlplane.yaml rename to k8s/apps/controlplane.yaml index 9247263..09abe8a 100644 --- a/k8s/controlplane.yaml +++ b/k8s/apps/controlplane.yaml @@ -30,6 +30,7 @@ spec: labels: app: controlplane platform: edcv + type: edcv-app spec: containers: - name: controlplane diff --git a/k8s/dataplane-config.yaml b/k8s/apps/dataplane-config.yaml similarity index 100% rename from k8s/dataplane-config.yaml rename to k8s/apps/dataplane-config.yaml diff --git a/k8s/dataplane.yaml b/k8s/apps/dataplane.yaml similarity index 99% rename from k8s/dataplane.yaml rename to k8s/apps/dataplane.yaml index 9af54b3..f2e270e 100644 --- a/k8s/dataplane.yaml +++ b/k8s/apps/dataplane.yaml @@ -30,6 +30,7 @@ spec: labels: app: dataplane platform: edcv + type: edcv-app spec: containers: - name: dataplane diff --git a/k8s/identityhub-config.yaml b/k8s/apps/identityhub-config.yaml similarity index 100% rename from k8s/identityhub-config.yaml rename to k8s/apps/identityhub-config.yaml diff --git a/k8s/identityhub.yaml b/k8s/apps/identityhub.yaml similarity index 99% rename from k8s/identityhub.yaml rename to k8s/apps/identityhub.yaml index d9909c8..6b60af0 100644 --- a/k8s/identityhub.yaml +++ b/k8s/apps/identityhub.yaml @@ -30,6 +30,7 @@ spec: labels: app: identityhub platform: edcv + type: edcv-app spec: containers: - name: identityhub diff --git a/k8s/issuerservice-config.yaml b/k8s/apps/issuerservice-config.yaml similarity index 100% rename from k8s/issuerservice-config.yaml rename to k8s/apps/issuerservice-config.yaml diff --git a/k8s/issuerservice.yaml b/k8s/apps/issuerservice.yaml similarity index 99% rename from k8s/issuerservice.yaml rename to k8s/apps/issuerservice.yaml index 2e807cd..a6e5e53 100644 --- a/k8s/issuerservice.yaml +++ b/k8s/apps/issuerservice.yaml @@ -30,6 +30,7 @@ spec: labels: app: issuerservice platform: edcv + type: edcv-app spec: containers: - name: issuerservice diff --git a/k8s/edcv-namespace.yaml b/k8s/base/edcv-namespace.yaml similarity index 100% rename from k8s/edcv-namespace.yaml rename to k8s/base/edcv-namespace.yaml diff --git a/k8s/keycloak.yaml b/k8s/base/keycloak.yaml similarity index 100% rename from k8s/keycloak.yaml rename to k8s/base/keycloak.yaml diff --git a/k8s/nats.yaml b/k8s/base/nats.yaml similarity index 100% rename from k8s/nats.yaml rename to k8s/base/nats.yaml diff --git a/k8s/postgres.yaml b/k8s/base/postgres.yaml similarity index 100% rename from k8s/postgres.yaml rename to k8s/base/postgres.yaml diff --git a/k8s/vault.yaml b/k8s/base/vault.yaml similarity index 97% rename from k8s/vault.yaml rename to k8s/base/vault.yaml index 7e58232..a531132 100644 --- a/k8s/vault.yaml +++ b/k8s/base/vault.yaml @@ -130,7 +130,7 @@ spec: { "role_type": "jwt", "user_claim": "participant_context_id", - "bound_issuer": "http://auth.vps.beardyinc.com/realms/edcv", + "bound_issuer": "http://vault.localhost/realms/edcv", "bound_claims": { "role": "participant" }, @@ -163,8 +163,7 @@ metadata: namespace: edc-v spec: rules: -# - host: vault.localhost - - host: vault.vps.beardyinc.com + - host: vault.localhost http: paths: - path: / diff --git a/k8s/kustomization.yml b/k8s/kustomization.yml index 5f00678..04f4020 100644 --- a/k8s/kustomization.yml +++ b/k8s/kustomization.yml @@ -12,17 +12,17 @@ # resources: - - edcv-namespace.yaml - - postgres.yaml - - nats.yaml - - keycloak.yaml - - vault.yaml - - controlplane-config.yaml - - controlplane.yaml - - dataplane-config.yaml - - dataplane.yaml - - issuerservice-config.yaml - - issuerservice.yaml - - identityhub-config.yaml - - identityhub.yaml + - base/edcv-namespace.yaml + - base/postgres.yaml + - base/nats.yaml + - base/keycloak.yaml + - base/vault.yaml + - apps/controlplane-config.yaml + - apps/controlplane.yaml + - apps/dataplane-config.yaml + - apps/dataplane.yaml + - apps/issuerservice-config.yaml + - apps/issuerservice.yaml + - apps/identityhub-config.yaml + - apps/identityhub.yaml diff --git a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create API Access Token (using Keycloak).bru b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create API Access Token (using Keycloak).bru index 82261f6..a523bb3 100644 --- a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create API Access Token (using Keycloak).bru +++ b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create API Access Token (using Keycloak).bru @@ -29,47 +29,47 @@ auth:oauth2 { body:json { { - "clientId": "{{participant_context_id}}", - "name": "{{participant_context_id}} Client", - "description": "Client for API access", - "enabled": true, - "secret": "{{participant_context_id}}-secret", - "protocol": "openid-connect", - "publicClient": false, - "serviceAccountsEnabled": true, - "standardFlowEnabled": false, - "directAccessGrantsEnabled": false, - "fullScopeAllowed": true, - "protocolMappers": [ - { - "name": "participantContextId", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "participant_context_id", - "claim.value": "{{participant_context_id}}", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "name": "role", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "role", - "claim.value": "participant", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] + "clientId": "{{participant_context_id}}", + "name": "{{participant_context_id}} Client", + "description": "Client for API access", + "enabled": true, + "secret": "{{participant_context_id}}-secret", + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "fullScopeAllowed": true, + "protocolMappers": [ + { + "name": "participantContextId", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "participant_context_id", + "claim.value": "{{participant_context_id}}", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "name": "role", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "role", + "claim.value": "participant", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] } } diff --git a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create ParticipantContext in IdentityHub.bru b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create ParticipantContext in IdentityHub.bru index e3d91a8..39deb72 100644 --- a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create ParticipantContext in IdentityHub.bru +++ b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create ParticipantContext in IdentityHub.bru @@ -39,7 +39,7 @@ body:json { "privateKeyAlias": "{{participant_context_did}}#key-1", "keyGeneratorParams": { "algorithm": "EDDSA", - "curce": "ed25519" + "curve": "ed25519" } }, "additionalProperties": { diff --git a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create Vault Access Token (using Keycloak).bru b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create Vault Access Token (using Keycloak).bru index b8a7fb0..ec4f17e 100644 --- a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create Vault Access Token (using Keycloak).bru +++ b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Create Vault Access Token (using Keycloak).bru @@ -29,47 +29,47 @@ auth:oauth2 { body:json { { - "clientId": "{{participant_context_id}}-vault", - "name": "{{participant_context_id}} Client", - "description": "Client for API access", - "enabled": true, - "secret": "{{participant_context_id}}-secret", - "protocol": "openid-connect", - "publicClient": false, - "serviceAccountsEnabled": true, - "standardFlowEnabled": false, - "directAccessGrantsEnabled": false, - "fullScopeAllowed": true, - "protocolMappers": [ - { - "name": "participantContextId", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "participant_context_id", - "claim.value": "{{participant_context_id}}", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "name": "role", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "role", - "claim.value": "participant", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] + "clientId": "{{participant_context_id}}-vault", + "name": "{{participant_context_id}} Client", + "description": "Client for API access", + "enabled": true, + "secret": "{{participant_context_id}}-secret", + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "fullScopeAllowed": true, + "protocolMappers": [ + { + "name": "participantContextId", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "participant_context_id", + "claim.value": "{{participant_context_id}}", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "name": "role", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "role", + "claim.value": "participant", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] } } diff --git a/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Get Credentials.bru b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Get Credentials.bru new file mode 100644 index 0000000..f8beb2e --- /dev/null +++ b/requests/EDC-V Onboarding/Create EDC-V ParticipantContext (Consumer)/Get Credentials.bru @@ -0,0 +1,22 @@ +meta { + name: Get Credentials + type: http + seq: 7 +} + +get { + url: {{baseURL}}/cs/api/identity/v1alpha/participants/{{participant_context_id_base64}}/credentials + body: none + auth: apikey +} + +auth:apikey { + key: x-api-key + value: c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo= + placement: header +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/tests/end2end/build.gradle.kts b/tests/end2end/build.gradle.kts new file mode 100644 index 0000000..8fa3ca7 --- /dev/null +++ b/tests/end2end/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + java +} + +dependencies { + runtimeOnly(libs.jackson.databind) + testImplementation(libs.edc.spi.catalog) + testImplementation(libs.edc.ih.spi.credentials) + testImplementation(libs.edc.ih.spi.participantcontext) + testImplementation(libs.edc.junit) + testImplementation(libs.jackson.annotations) + testImplementation(libs.awaitility) + testImplementation(libs.restAssured) +} + +edcBuild { + publish.set(false) +} diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java new file mode 100644 index 0000000..7b9dd19 --- /dev/null +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.jad.tests; + +import org.eclipse.edc.identityhub.spi.participantcontext.model.CreateParticipantContextResponse; +import org.eclipse.edc.jad.tests.model.CatalogResponse; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Base64; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakAdminToken; +import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakUser; +import static org.eclipse.edc.jad.tests.KeycloakApi.getAccessToken; + +/** + * This test class executes a series of REST requests against several components to verify that an end-to-end + * data transfer works. It assumes that the deployment to a local KinD cluster has already been performed, but no other + * manipulation of the cluster has been done. + *
+ */
+@EndToEndTest
+public class DataTransferTest {
+
+ public static final String ISSUER_CLIENT_ID = "issuer";
+ public static final String ISSUER_CLIENT_SECRET = "issuer-secret";
+
+ static final String BASE_URL = "http://127.0.0.1";
+ static final String API_ADMIN_KEY = "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=";
+
+ static String loadResourceFile(String resourceName) {
+ try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) {
+ if (is == null) {
+ throw new RuntimeException("Resource not found: " + resourceName);
+ }
+ return new String(is.readAllBytes());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ void testDataTransfer() {
+ var monitor = new ConsoleMonitor(ConsoleMonitor.Level.DEBUG, true);
+ var kcAdminToken = createKeycloakAdminToken();
+
+ //create issuer user in KC
+ monitor.withPrefix("Issuer").info("Creating issuer user in Keycloak");
+ createKeycloakUser(ISSUER_CLIENT_ID, ISSUER_CLIENT_ID, ISSUER_CLIENT_SECRET, "participant", kcAdminToken);
+ var issuerTenant = createIssuerTenant();
+ var participantIdBase64 = Base64.getEncoder().encodeToString(ISSUER_CLIENT_ID.getBytes());
+ monitor.withPrefix("Issuer").info("Creating attestation and credential definitions");
+ var attestationDefId = createAttestationDefinition(participantIdBase64, issuerTenant.apiKey());
+ var credentialDefId = createCredentialDefId(attestationDefId, participantIdBase64, issuerTenant.apiKey());
+
+ // onboard consumer
+ monitor.info("Onboarding consumer");
+ var po = new ParticipantOnboarding("consumer", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:consumer", ISSUER_CLIENT_ID, issuerTenant.apiKey(), monitor.withPrefix("Consumer"));
+ po.execute(credentialDefId);
+
+ // onboard provider
+ monitor.info("Onboarding provider");
+ var providerPo = new ParticipantOnboarding("provider", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider", ISSUER_CLIENT_ID, issuerTenant.apiKey(), monitor.withPrefix("Provider"));
+ providerPo.execute(credentialDefId);
+
+ // perform data transfer
+ monitor.info("Starting data transfer");
+ var accessToken = getAccessToken("consumer", "consumer-secret", "management-api:read");
+ var catalog = given()
+ .baseUri(BASE_URL)
+ .auth().oauth2(accessToken.accessToken())
+ .contentType("application/json")
+ .body("""
+ {
+ "counterPartyDid": "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider"
+ }
+ """)
+ .post("/cp/api/mgmt/v1alpha/participants/consumer/catalog")
+ .then()
+ .statusCode(200)
+ .extract().body()
+ .as(CatalogResponse.class);
+
+ monitor.info("Catalog received, starting data transfer");
+ var offerId = catalog.datasets().get(0).offers().get(0).id();
+ assertThat(offerId).isNotNull();
+
+ //download dummy data
+ var jsonResponse = given()
+ .baseUri(BASE_URL)
+ .auth().oauth2(getAccessToken("consumer", "consumer-secret", "management-api:write").accessToken())
+ .body("""
+ {
+ "providerId":"did:web:identityhub.edc-v.svc.cluster.local%%3A7083:provider",
+ "policyId": "%s"
+ }
+ """.formatted(offerId))
+ .contentType("application/json")
+ .post("/cp/api/mgmt/v1alpha/participants/consumer/data")
+ .then()
+ .statusCode(200)
+ .extract().body().asPrettyString();
+ assertThat(jsonResponse).isNotNull();
+ }
+
+ private String createCredentialDefId(String attestationDefId, String participantIdBase64, String apiKey) {
+ var template = loadResourceFile("membership_def.json");
+
+ var id = UUID.randomUUID().toString();
+ template = template.replace("{{attestation_id}}", attestationDefId);
+ template = template.replace("{{id}}", id);
+
+ given()
+ .baseUri(BASE_URL)
+ .header("x-api-key", apiKey)
+ .contentType("application/json")
+ .body(template)
+ .post("/issuer/admin/api/admin/v1alpha/participants/%s/credentialdefinitions".formatted(participantIdBase64))
+ .then()
+ .log().ifValidationFails()
+ .statusCode(201);
+ return id;
+ }
+
+ private String createAttestationDefinition(String participantIdBase64, String apiKey) {
+ var id = UUID.randomUUID().toString();
+ var body = """
+ {
+ "attestationType": "membership",
+ "configuration": {},
+ "id": "%s"
+ }
+ """.formatted(id);
+ given()
+ .baseUri(BASE_URL)
+ .header("x-api-key", apiKey)
+ .contentType("application/json")
+ .body(body)
+ .post("/issuer/admin/api/admin/v1alpha/participants/%s/attestations".formatted(participantIdBase64))
+ .then()
+ .log().ifValidationFails()
+ .statusCode(201);
+ return id;
+ }
+
+ private CreateParticipantContextResponse createIssuerTenant() {
+ var template = loadResourceFile("create_participant_issuerservice.json");
+
+ template = template.replace("{{issuer_clientId}}", ISSUER_CLIENT_ID);
+ template = template.replace("{{issuer_clientSecret}}", ISSUER_CLIENT_SECRET);
+
+ return given()
+ .baseUri(BASE_URL)
+ .header("x-api-key", API_ADMIN_KEY)
+ .contentType("application/json")
+ .body(template)
+ .post("/issuer/cs/api/identity/v1alpha/participants")
+ .then()
+ .statusCode(200)
+ .extract()
+ .body().as(CreateParticipantContextResponse.class);
+ }
+}
diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java
new file mode 100644
index 0000000..85a6e14
--- /dev/null
+++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2025 Metaform Systems, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License, Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Contributors:
+ * Metaform Systems, Inc. - initial API and implementation
+ *
+ */
+
+package org.eclipse.edc.jad.tests;
+
+import org.eclipse.edc.jad.tests.model.AccessToken;
+
+import static io.restassured.RestAssured.given;
+import static org.eclipse.edc.jad.tests.DataTransferTest.loadResourceFile;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.equalTo;
+
+public class KeycloakApi {
+ static final String KEYCLOAK_URL = "http://keycloak.localhost";
+ private static final String KEYCLOAK_ADMIN_USER = "admin";
+ private static final String KEYCLOAK_ADMIN_PASSWORD = "admin";
+
+ static void createKeycloakUser(String name, String clientId, String secret, String role, String token) {
+ var template = loadResourceFile("create_keycloak_user.json");
+ template = template
+ .replace("{{issuer_name}}", name)
+ .replace("{{issuer_clientId}}", clientId)
+ .replace("{{issuer_clientSecret}}", secret)
+ .replace("{{role}}", role);
+
+ given()
+ .baseUri(KEYCLOAK_URL)
+ .contentType("application/json")
+ .auth().oauth2(token)
+ .body(template)
+ .post("/admin/realms/edcv/clients")
+ .then()
+ .log().ifError()
+ .statusCode(anyOf(equalTo(201), equalTo(409)));
+
+ }
+
+ static String createKeycloakAdminToken() {
+ var at = given()
+ .baseUri(KEYCLOAK_URL)
+ .contentType("application/x-www-form-urlencoded")
+ .formParam("username", KEYCLOAK_ADMIN_USER)
+ .formParam("password", KEYCLOAK_ADMIN_PASSWORD)
+ .formParam("client_id", "admin-cli")
+ .formParam("grant_type", "password")
+ .post("/realms/master/protocol/openid-connect/token")
+ .then()
+ .statusCode(200)
+ .extract()
+ .body()
+ .as(AccessToken.class);
+ return at.accessToken();
+ }
+
+ static AccessToken getAccessToken(String clientId, String clientSecret, String scope) {
+ return given()
+ .baseUri(KEYCLOAK_URL)
+ .contentType("application/x-www-form-urlencoded")
+ .formParam("client_id", clientId)
+ .formParam("client_secret", clientSecret)
+ .formParam("grant_type", "client_credentials")
+ .formParam("scope", scope)
+ .post("/realms/edcv/protocol/openid-connect/token")
+ .then()
+ .statusCode(200)
+ .extract()
+ .body()
+ .as(AccessToken.class);
+ }
+}
diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java
new file mode 100644
index 0000000..d2e3b7b
--- /dev/null
+++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2025 Metaform Systems, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License, Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Contributors:
+ * Metaform Systems, Inc. - initial API and implementation
+ *
+ */
+
+package org.eclipse.edc.jad.tests;
+
+import org.eclipse.edc.identityhub.spi.participantcontext.model.CreateParticipantContextResponse;
+import org.eclipse.edc.jad.tests.model.HolderCredentialRequestDto;
+import org.eclipse.edc.spi.monitor.Monitor;
+
+import java.util.Base64;
+import java.util.UUID;
+
+import static io.restassured.RestAssured.given;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.eclipse.edc.jad.tests.DataTransferTest.API_ADMIN_KEY;
+import static org.eclipse.edc.jad.tests.DataTransferTest.BASE_URL;
+import static org.eclipse.edc.jad.tests.DataTransferTest.loadResourceFile;
+import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakAdminToken;
+import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakUser;
+import static org.eclipse.edc.jad.tests.KeycloakApi.getAccessToken;
+
+public record ParticipantOnboarding(String participantContextId, String participantContextDid, String issuerId,
+ String issuerApiKey, Monitor monitor) {
+
+ public String participantContextIdBase64() {
+ return Base64.getEncoder().encodeToString(participantContextId.getBytes());
+ }
+
+ public void execute(String credentialDefinitionId) {
+ var accessToken = createKeycloakAdminToken();
+ monitor.info("Configuring Vault Access in Keycloak");
+ createKeycloakUser(participantContextId + "-vault", participantContextId, participantContextId + "-secret", "participant", accessToken);
+ monitor.info("Configuring API Access in Keycloak");
+ createKeycloakUser(participantContextId, participantContextId, participantContextId + "-secret", "participant", accessToken);
+
+ monitor.info("Create holder in IssuerService");
+ createHolder();
+
+ monitor.info("Onboard onto IdentityHub");
+ var ihPc = createParticipantInIdentityHub();
+ monitor.info("Onboard onto Control Plane");
+ createParticipantInControlPlane(ihPc);
+
+ monitor.info("Create credential request");
+ var holderPid = createCredentialRequest(ihPc.apiKey(), credentialDefinitionId);
+
+ monitor.info("Wait for credential issuance");
+ waitForCredentialIssuance(ihPc.apiKey(), holderPid);
+ monitor.info("Credential issued successfully");
+ }
+
+ private void waitForCredentialIssuance(String apiKey, String holderPid) {
+ await().atMost(20, SECONDS)
+ .pollInterval(1, SECONDS).until(() -> {
+ var response = given()
+ .baseUri(BASE_URL)
+ .contentType("application/json")
+ .header("x-api-key", apiKey)
+ .get("/cs/api/identity/v1alpha/participants/%s/credentials/request/%s".formatted(participantContextIdBase64(), holderPid))
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .extract()
+ .body()
+ .as(HolderCredentialRequestDto.class);
+ return "ISSUED".equals(response.status());
+ });
+ }
+
+ private String createCredentialRequest(String apiKey, String credentialDefinitionId) {
+ var holderPid = UUID.randomUUID().toString();
+ given()
+ .baseUri(BASE_URL)
+ .contentType("application/json")
+ .header("x-api-key", apiKey)
+ .body("""
+ {
+ "issuerDid": "did:web:issuerservice.edc-v.svc.cluster.local%%3A10016:issuer",
+ "holderPid": "%s",
+ "credentials": [{
+ "format": "VC1_0_JWT",
+ "type": "MembershipCredential",
+ "id": "%s"
+ }]
+ }
+ """.formatted(holderPid, credentialDefinitionId))
+ .post("/cs/api/identity/v1alpha/participants/%s/credentials/request".formatted(participantContextIdBase64()))
+ .then()
+ .log().ifValidationFails()
+ .statusCode(201);
+ return holderPid;
+ }
+
+ /**
+ * Onboards the participant in the control plane.
+ *
+ * @deprecated will be replaced by the proper Management API call in due time
+ */
+ @Deprecated
+ private void createParticipantInControlPlane(CreateParticipantContextResponse identityhubClient) {
+ var template = loadResourceFile("create_participant_controlplane.json");
+ var requestBody = template.replace("{{participant_context_id}}", participantContextId)
+ .replace("{{participant_context_did}}", participantContextDid)
+ .replace("{{tenant_clientSecret}}", identityhubClient.clientSecret())
+ .replace("{{tenant_clientId}}", identityhubClient.clientId());
+
+ var accessToken = getAccessToken("admin", "edc-v-admin-secret", "management-api:read management-api:write identity-api:read identity-api:write").accessToken();
+ given()
+ .baseUri(BASE_URL)
+ .contentType("application/json")
+ .auth().oauth2(accessToken)
+ .body(requestBody)
+ .post("/cp/api/mgmt/v1alpha/participants")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(201);
+ }
+
+ private CreateParticipantContextResponse createParticipantInIdentityHub() {
+ var template = loadResourceFile("create_participant_identityhub.json");
+ var requestBody = template
+ .replace("{{participant_context_id}}", participantContextId)
+ .replace("{{participant_context_did}}", participantContextDid)
+ .replace("{{participant_context_id_base64}}", participantContextIdBase64());
+
+ return given()
+ .baseUri(BASE_URL)
+ .contentType("application/json")
+ .header("x-api-key", API_ADMIN_KEY)
+ .body(requestBody)
+ .post("/cs/api/identity/v1alpha/participants")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .extract()
+ .body().as(CreateParticipantContextResponse.class);
+ }
+
+ private void createHolder() {
+ given()
+ .baseUri(BASE_URL)
+ .contentType("application/json")
+ .header("x-api-key", issuerApiKey)
+ .body("""
+ {
+ "did": "%s",
+ "holderId": "%s",
+ "name": "%s tenant"
+ }""".formatted(participantContextDid, participantContextDid, participantContextId))
+ .post("/issuer/admin/api/admin/v1alpha/participants/%s/holders".formatted(issuerIdBase64()))
+ .then()
+ .statusCode(201);
+ }
+
+ private String issuerIdBase64() {
+ return Base64.getEncoder().encodeToString(issuerId.getBytes());
+ }
+}
diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/AccessToken.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/AccessToken.java
new file mode 100644
index 0000000..a5b299c
--- /dev/null
+++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/AccessToken.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Metaform Systems, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License, Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Contributors:
+ * Metaform Systems, Inc. - initial API and implementation
+ *
+ */
+
+package org.eclipse.edc.jad.tests.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AccessToken(@JsonProperty("access_token") String accessToken) {
+}
diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CatalogResponse.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CatalogResponse.java
new file mode 100644
index 0000000..1693605
--- /dev/null
+++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CatalogResponse.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2025 Metaform Systems, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License, Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Contributors:
+ * Metaform Systems, Inc. - initial API and implementation
+ *
+ */
+
+package org.eclipse.edc.jad.tests.model;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * This is a minimal version of an EDC Catalog, ignoring most unneeded fields
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CatalogResponse(@JsonProperty("@id") String id,
+ @JsonProperty("@type") String type,
+ @JsonProperty(value = "dataset", defaultValue = "[]") List