diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index c32e4be..4b0823a 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -38,7 +38,6 @@ concurrency: jobs: E2E-Tests: runs-on: ubuntu-latest - if: 1 ==2 # disable for now steps: - uses: actions/checkout@v6 - uses: eclipse-edc/.github/.github/actions/setup-build@main @@ -88,12 +87,16 @@ jobs: - name: "Deploy JAD applications" run: |- # this is crucial - without it, KinD would take the prebuilt images from GHCR - grep -rlZ "imagePullPolicy:.*Always" . | xargs --null sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" + sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" k8s/apps/controlplane.yaml + sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" k8s/apps/dataplane.yaml + sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" k8s/apps/issuerservice.yaml + sed -i "s/imagePullPolicy:.*Always/imagePullPolicy: Never/g" k8s/apps/identityhub.yaml kubectl apply -f k8s/apps + + # wait until all init jobs are done kubectl wait --namespace edc-v \ - --for=condition=ready pod \ - --selector=type=edcv-app \ + --for=condition=complete job --all \ --timeout=90s - name: "Run E2E Test" diff --git a/k8s/apps/controlplane.yaml b/k8s/apps/controlplane.yaml index 7537a17..98b2115 100644 --- a/k8s/apps/controlplane.yaml +++ b/k8s/apps/controlplane.yaml @@ -35,7 +35,7 @@ spec: containers: - name: controlplane image: ghcr.io/metaform/jad/controlplane:latest - imagePullPolicy: Never + imagePullPolicy: Always envFrom: - configMapRef: { name: controlplane-config } ports: diff --git a/k8s/apps/participant-manager.yaml b/k8s/apps/participant-manager.yaml index b5f2eef..a5e3357 100644 --- a/k8s/apps/participant-manager.yaml +++ b/k8s/apps/participant-manager.yaml @@ -55,12 +55,12 @@ metadata: name: participant-manager namespace: edc-v spec: - type: NodePort selector: app: participant-manager ports: - port: 8080 targetPort: 8080 + name: http --- apiVersion: networking.k8s.io/v1 diff --git a/k8s/apps/tenant-manager.yaml b/k8s/apps/tenant-manager.yaml index 216dc37..c3c704a 100644 --- a/k8s/apps/tenant-manager.yaml +++ b/k8s/apps/tenant-manager.yaml @@ -55,12 +55,12 @@ metadata: name: tenant-manager namespace: edc-v spec: - type: NodePort selector: app: tenant-manager ports: - port: 8080 targetPort: 8080 + name: http --- apiVersion: networking.k8s.io/v1 diff --git a/k8s/base/keycloak.yaml b/k8s/base/keycloak.yaml index b63b947..089eb08 100644 --- a/k8s/base/keycloak.yaml +++ b/k8s/base/keycloak.yaml @@ -270,6 +270,7 @@ data: "groups": [], "eventsEnabled": false } + --- apiVersion: v1 kind: Service diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/HolderCredentialRequestDto.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java similarity index 51% rename from tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/HolderCredentialRequestDto.java rename to tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java index 5379b77..cc5a2b7 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/HolderCredentialRequestDto.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java @@ -12,12 +12,13 @@ * */ -package org.eclipse.edc.jad.tests.model; +package org.eclipse.edc.jad.tests; -import org.eclipse.edc.identityhub.spi.credential.request.model.RequestedCredential; - -import java.util.List; - -public record HolderCredentialRequestDto(String issuerDid, String holderPid, String issuerPid, String status, - List typesAndFormats) { -} \ No newline at end of file +public interface Constants { + String APPLICATION_JSON = "application/json"; + String TM_BASE_URL = "http://tm.localhost"; + String PM_BASE_URL = "http://pm.localhost"; + String VAULT_URL = "http://vault.localhost"; + String BASE_URL = "http://127.0.0.1"; + String KEYCLOAK_URL = "http://keycloak.localhost"; +} diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java index 8e840b3..b031951 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferTest.java @@ -14,21 +14,21 @@ 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.jad.tests.model.ClientCredentials; 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 java.time.Instant; 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.Constants.APPLICATION_JSON; +import static org.eclipse.edc.jad.tests.Constants.BASE_URL; +import static org.eclipse.edc.jad.tests.Constants.TM_BASE_URL; import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakToken; -import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakUser; import static org.eclipse.edc.jad.tests.KeycloakApi.getAccessToken; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; @@ -41,10 +41,8 @@ @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"; + private static final String VAULT_TOKEN = "root"; static String loadResourceFile(String resourceName) { try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { @@ -60,57 +58,38 @@ static String loadResourceFile(String resourceName) { @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 provisionerToken = createKeycloakToken("provisioner", "provisioner-secret", "issuer-admin-api:write", "identity-api:write", "management-api:write", "identity-api:read"); - createIssuerTenant(provisionerToken); - createCelExpression(provisionerToken); - var participantIdBase64 = Base64.getEncoder().encodeToString(ISSUER_CLIENT_ID.getBytes()); - monitor.withPrefix("Issuer").info("Creating attestation and credential definitions"); + var adminToken = createKeycloakToken("admin", "edc-v-admin-secret", "issuer-admin-api:write", "identity-api:write", "management-api:write", "identity-api:read"); + createCelExpression(adminToken); - var tenantToken = createKeycloakToken(ISSUER_CLIENT_ID, ISSUER_CLIENT_SECRET, "issuer-admin-api:write", "identity-api:write"); - var attestationDefId = createAttestationDefinition(participantIdBase64, tenantToken); - var credentialDefId = createCredentialDefId(attestationDefId, participantIdBase64, tenantToken); + monitor.info("Create cell and dataspace profile"); + var cellId = createCell(); + var dataspaceProfileId = createDataspaceProfile(); + deployDataspaceProfile(dataspaceProfileId, cellId); // onboard consumer monitor.info("Onboarding consumer"); - var po = new ParticipantOnboarding("consumer", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:consumer", ISSUER_CLIENT_ID, provisionerToken, monitor.withPrefix("Consumer")); - po.execute(credentialDefId); + var po = new ParticipantOnboarding("consumer", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:consumer", VAULT_TOKEN, monitor.withPrefix("Consumer")); + var consumerCredentials = po.execute(cellId); // onboard provider monitor.info("Onboarding provider"); - var providerPo = new ParticipantOnboarding("provider", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider", ISSUER_CLIENT_ID, provisionerToken, monitor.withPrefix("Provider")); - providerPo.execute(credentialDefId); + var providerPo = new ParticipantOnboarding("provider", "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider", VAULT_TOKEN, monitor.withPrefix("Provider")); + var providerCredentials = providerPo.execute(cellId); // seed provider monitor.info("Seeding provider"); - var providerAccesstoken = getAccessToken("provider", "provider-secret", "management-api:write"); + var providerAccessToken = getAccessToken(providerCredentials.clientId(), providerCredentials.clientSecret(), "management-api:write").accessToken(); - var assetId = createAsset(providerAccesstoken.accessToken()); - var policyDefId = createPolicyDef(providerAccesstoken.accessToken()); - createContractDef(providerAccesstoken.accessToken(), policyDefId, assetId); + var assetId = createAsset(providerCredentials.clientId(), providerAccessToken); + var policyDefId = createPolicyDef(providerCredentials.clientId(), providerAccessToken); + createContractDef(providerCredentials.clientId(), providerAccessToken, policyDefId, assetId); + registerDataplane(providerCredentials.clientId(), providerAccessToken); // 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); + var catalog = fetchCatalog(consumerCredentials); monitor.info("Catalog received, starting data transfer"); var offerId = catalog.datasets().get(0).offers().get(0).id(); @@ -119,7 +98,7 @@ void testDataTransfer() { //download dummy data var jsonResponse = given() .baseUri(BASE_URL) - .auth().oauth2(getAccessToken("consumer", "consumer-secret", "management-api:write").accessToken()) + .auth().oauth2(getAccessToken(consumerCredentials.clientId(), consumerCredentials.clientSecret(), "management-api:write").accessToken()) .body(""" { "providerId":"did:web:identityhub.edc-v.svc.cluster.local%%3A7083:provider", @@ -127,71 +106,100 @@ void testDataTransfer() { } """.formatted(offerId)) .contentType("application/json") - .post("/cp/api/mgmt/v1alpha/participants/consumer/data") + .post("/cp/api/mgmt/v1alpha/participants/%s/data".formatted(consumerCredentials.clientId())) .then() .statusCode(200) .extract().body().asPrettyString(); assertThat(jsonResponse).isNotNull(); } - private String createCredentialDefId(String attestationDefId, String participantIdBase64, String accessToken) { - var template = loadResourceFile("membership_def.json"); - var id = UUID.randomUUID().toString(); - template = template.replace("{{attestation_id}}", attestationDefId); - template = template.replace("{{id}}", id); + private CatalogResponse fetchCatalog(ClientCredentials consumerCredentials) { + var accessToken = getAccessToken(consumerCredentials.clientId(), consumerCredentials.clientSecret(), "management-api:read"); - given() + return given() .baseUri(BASE_URL) - .auth().oauth2(accessToken) + .auth().oauth2(accessToken.accessToken()) .contentType("application/json") - .body(template) - .post("/issuer/admin/api/admin/v1alpha/participants/%s/credentialdefinitions".formatted(participantIdBase64)) + .body(""" + { + "counterPartyDid": "did:web:identityhub.edc-v.svc.cluster.local%3A7083:provider" + } + """) + .post("/cp/api/mgmt/v1alpha/participants/%s/catalog".formatted(consumerCredentials.clientId())) .then() - .log().ifValidationFails() - .statusCode(201); - return id; + .statusCode(200) + .extract().body() + .as(CatalogResponse.class); } - private String createAttestationDefinition(String participantIdBase64, String accessToken) { - var id = UUID.randomUUID().toString(); - var body = """ - { - "attestationType": "membership", - "configuration": {}, - "id": "%s" - } - """.formatted(id); - given() - .baseUri(BASE_URL) - .auth().oauth2(accessToken) - .contentType("application/json") - .body(body) - .post("/issuer/admin/api/admin/v1alpha/participants/%s/attestations".formatted(participantIdBase64)) + private String createDataspaceProfile() { + return given() + .baseUri(TM_BASE_URL) + .contentType(APPLICATION_JSON) + .body(""" + { + "artifacts": [], + "properties": {} + } + """) + .post("/api/v1alpha1/dataspace-profiles") .then() + .statusCode(201) .log().ifValidationFails() - .statusCode(201); - return id; + .extract().body().jsonPath().getString("id"); } - private CreateParticipantContextResponse createIssuerTenant(String accessToken) { - var template = loadResourceFile("create_participant_issuerservice.json"); - - template = template.replace("{{issuer_clientId}}", ISSUER_CLIENT_ID); - template = template.replace("{{issuer_clientSecret}}", ISSUER_CLIENT_SECRET); - + /** + * Creates a cell in CFM. + * + * @return the Cell ID + */ + private String createCell() { return given() - .baseUri(BASE_URL) - .auth().oauth2(accessToken) - .contentType("application/json") - .body(template) - .post("/issuer/cs/api/identity/v1alpha/participants") + .contentType(APPLICATION_JSON) + .body(""" + { + "properties": { + "cellPurpose": "e2e-test" + }, + "state": "active", + "stateTimestamp": "%s" + } + """.formatted(Instant.now().toString())) + .post(TM_BASE_URL + "/api/v1alpha1/cells") .then() - .statusCode(200) - .extract() - .body().as(CreateParticipantContextResponse.class); + .statusCode(201) + .extract().jsonPath().getString("id"); + } + + /** + * Deploys a dataspace profile in CFM. + * + * @param dataspaceProfileId the dataspace profile ID to deploy + * @param cellId the cell ID to deploy the profile to + */ + private void deployDataspaceProfile(String dataspaceProfileId, String cellId) { + given() + .baseUri(TM_BASE_URL) + .contentType(APPLICATION_JSON) + .body(""" + { + "profileId": "%s", + "cellId": "%s" + } + """.formatted(dataspaceProfileId, cellId)) + .post("/api/v1alpha1/dataspace-profiles/%s/deployments".formatted(dataspaceProfileId)) + .then() + .log().ifValidationFails() + .statusCode(202); } + /** + * Creates a Common Expression Language (CEL) entry in the control plane + * + * @param accessToken OAuth2 token + */ private void createCelExpression(String accessToken) { var template = loadResourceFile("create_cel_expression.json"); @@ -205,33 +213,58 @@ private void createCelExpression(String accessToken) { .statusCode(200); } - private String createAsset(String accessToken) { + /** + * Registers a data plane for a new participant context. This is a bit of a workaround, until Dataplane Signaling is fully implemented. + * Check also the {@code DataplaneRegistrationApiController} in the {@code extensions/api/mgmt} directory + * + * @param participantContextId Participant context for which the data plane should be registered. + * @param accessToken OAuth2 token + */ + private void registerDataplane(String participantContextId, String accessToken) { + given() + .baseUri(BASE_URL) + .contentType(APPLICATION_JSON) + .auth().oauth2(accessToken) + .body(""" + { + "allowedSourceTypes": [ "HttpData" ], + "allowedTransferTypes": [ "HttpData-PULL" ], + "url": "http://dataplane.edc-v.svc.cluster.local:8083/api/control/v1/dataflows" + } + """) + .post("/cp/api/mgmt/v4alpha/dataplanes/%s".formatted(participantContextId)) + .then() + .log().ifValidationFails() + .statusCode(204); + } + + private String createAsset(String participantContextId, String accessToken) { var template = loadResourceFile("asset.json"); return given() .baseUri(BASE_URL) .auth().oauth2(accessToken) .contentType("application/json") .body(template) - .post("/cp/api/mgmt/v4alpha/participants/provider/assets") + .post("/cp/api/mgmt/v4alpha/participants/%s/assets".formatted(participantContextId)) .then() .statusCode(200) .extract().jsonPath().getString(ID); } - private String createPolicyDef(String accessToken) { + private String createPolicyDef(String participantContextId, String accessToken) { var template = loadResourceFile("policy-def.json"); return given() .baseUri(BASE_URL) .auth().oauth2(accessToken) .contentType("application/json") .body(template) - .post("/cp/api/mgmt/v4alpha/participants/provider/policydefinitions") + .post("/cp/api/mgmt/v4alpha/participants/%s/policydefinitions".formatted(participantContextId)) .then() .statusCode(200) .extract().jsonPath().getString(ID); } - private String createContractDef(String accessToken, String policyDefId, String assetId) { + private String createContractDef(String participantContextId, String accessToken, String policyDefId, String assetId) { var template = loadResourceFile("contract-def.json"); template = template.replace("{{policy_def_id}}", policyDefId); @@ -242,7 +275,7 @@ private String createContractDef(String accessToken, String policyDefId, String .auth().oauth2(accessToken) .contentType("application/json") .body(template) - .post("/cp/api/mgmt/v4alpha/participants/provider/contractdefinitions") + .post("/cp/api/mgmt/v4alpha/participants/%s/contractdefinitions".formatted(participantContextId)) .then() .statusCode(200) .extract().jsonPath().getString(ID); 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 index 403bebe..706a1f3 100644 --- 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 @@ -17,12 +17,12 @@ import org.eclipse.edc.jad.tests.model.AccessToken; import static io.restassured.RestAssured.given; +import static org.eclipse.edc.jad.tests.Constants.KEYCLOAK_URL; 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"; 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 index 1f2be9e..cd33c38 100644 --- 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 @@ -14,157 +14,189 @@ 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.jad.tests.model.ClientCredentials; +import org.eclipse.edc.jad.tests.model.Orchestration; import org.eclipse.edc.spi.monitor.Monitor; import java.util.Base64; -import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import static io.restassured.RestAssured.given; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -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; +import static org.eclipse.edc.jad.tests.Constants.BASE_URL; +import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakToken; -public record ParticipantOnboarding(String participantContextId, String participantContextDid, String issuerId, - String accessToken, Monitor monitor) { +/** + * Takes care of onboarding a single participant into the test Dataspace + * + * @param participantName A human-readable name for the participant. Will be used to create a tenant in CFM + * @param participantContextDid The Web:DID of the participant. + * @param vaultToken A token for Hashicorp Vault which grants access to the {@code v1/secret} secret engine. + * @param monitor A monitor for some logging + */ +public record ParticipantOnboarding(String participantName, String participantContextDid, + String vaultToken, Monitor monitor) { - public String participantContextIdBase64() { - return Base64.getEncoder().encodeToString(participantContextId.getBytes()); - } + public ClientCredentials execute(String cellId) { - 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("Creating tenant for %s".formatted(participantName)); + var tenantId = createTenant(participantName); + monitor.info("Deploy dataspace profile"); + monitor.info("Deploy participant profile"); + var profileId = deployParticipantProfile(tenantId, cellId, participantContextDid); - monitor.info("Create holder in IssuerService"); - createHolder(); + monitor.info("Waiting for orchestration to complete"); + var orchestrationId = queryOrchestrationByProfileId(profileId); + var orchestration = getOrchestrationById(orchestrationId); + monitor.info("Orchestration completed. Reading participant access credentials"); + var participantContextId = orchestration.getOutputData().get("participantContextId").toString(); + var secret = getVaultSecret(participantContextId); - monitor.info("Onboard onto IdentityHub"); - var ihPc = createParticipantInIdentityHub(); - monitor.info("Onboard onto Control Plane"); - createParticipantInControlPlane(ihPc); + var token = createKeycloakToken(participantContextId, secret, "identity-api:write", "identity-api:read"); - monitor.info("Create credential request"); - var userToken = KeycloakApi.createKeycloakToken(participantContextId, participantContextId + "-secret", "identity-api:write", "identity-api:read"); - var holderPid = createCredentialRequest(userToken, credentialDefinitionId); + monitor.info("Waiting for credential issuance"); + assertThat(orchestration.getOutputData()) + .hasFieldOrProperty("holderPid") + .hasFieldOrProperty("participantContextId") + .hasFieldOrProperty("credentialRequest"); - monitor.info("Wait for credential issuance"); - waitForCredentialIssuance(userToken, holderPid); - monitor.info("Credential issued successfully"); - } + var holderPid = orchestration.getOutputData().get("holderPid"); + assertThat(holderPid).withFailMessage(() -> "holderPid should be on the Orchestration's output data").isNotNull(); + waitForCredentialIssuance(participantContextId, token, holderPid.toString()); - private void waitForCredentialIssuance(String userToken, String holderPid) { - await().atMost(20, SECONDS) - .pollInterval(1, SECONDS).until(() -> { - var response = given() - .baseUri(BASE_URL) - .contentType("application/json") - .auth().oauth2(userToken) - .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()); - }); + + return new ClientCredentials(participantContextId, secret); } - private String createCredentialRequest(String userToken, String credentialDefinitionId) { - var holderPid = UUID.randomUUID().toString(); - given() - .baseUri(BASE_URL) - .contentType("application/json") - .auth().oauth2(userToken) - .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())) + //we could the full HashicorpVault for this, but a REST request is simpler here + private String getVaultSecret(String participantContextId) { + return given() + .baseUri(Constants.VAULT_URL) + .header("X-Vault-Token", vaultToken) + .get("/v1/secret/data/%s".formatted(participantContextId)) .then() .log().ifValidationFails() - .statusCode(201); - return holderPid; + .statusCode(200) + .extract().body().jsonPath().getString("data.data.content"); } /** - * Onboards the participant in the control plane. + * Retrieves an Orchestration object by its ID. * - * @deprecated will be replaced by the proper Management API call in due time + * @param orchestrationId the unique identifier of the orchestration to retrieve + * @return the Orchestration object */ - @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") + private Orchestration getOrchestrationById(String orchestrationId) { + return given() + .baseUri(Constants.PM_BASE_URL) + .contentType(Constants.APPLICATION_JSON) + .get("/api/v1alpha1/orchestrations/%s".formatted(orchestrationId)) .then() .log().ifValidationFails() - .statusCode(201); + .statusCode(200) + .extract().body().as(Orchestration.class); + } + + /** + * Queries an orchestration object by its correlation ID, which is the participantProfileID + * + * @param participantProfileId the participant profile ID + * @return the orchestration ID + */ + private String queryOrchestrationByProfileId(String participantProfileId) { + var orchestrationId = new AtomicReference(); + await().atMost(20, SECONDS) + .pollInterval(1, SECONDS).untilAsserted(() -> { + var body = given() + .baseUri(Constants.PM_BASE_URL) + .contentType(Constants.APPLICATION_JSON) + .body(""" + { + "predicate": "correlationId = '%s'" + } + """.formatted(participantProfileId)) + .post("/api/v1alpha1/orchestrations/query") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body(); + orchestrationId.set(body.jsonPath().getString("[0].id")); + + }); + return orchestrationId.get(); } - 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()); + /** + * Deploys (and creates) a participant profile in CFM. + * + * @param tenantId the tenant ID. + * @param cellId the cell ID. + * @param participantContextDid the Web:DID of the participant. + * @return the participant profile ID. + */ + private String deployParticipantProfile(String tenantId, String cellId, String participantContextDid) { return given() - .baseUri(BASE_URL) - .contentType("application/json") - .auth().oauth2(accessToken) - .body(requestBody) - .post("/cs/api/identity/v1alpha/participants") + .baseUri(Constants.TM_BASE_URL) + .contentType(Constants.APPLICATION_JSON) + .body(""" + { + "identifier": "%s", + "properties": {}, + "cellId": "%s" + } + """.formatted(participantContextDid, cellId)) + .post("/api/v1alpha1/tenants/%s/participant-profiles".formatted(tenantId)) .then() .log().ifValidationFails() - .statusCode(200) - .extract() - .body().as(CreateParticipantContextResponse.class); + .statusCode(202) + .extract().body().jsonPath().getString("id"); } - private void createHolder() { - given() - .baseUri(BASE_URL) - .contentType("application/json") - .auth().oauth2(accessToken) + + /** + * Creates a tenant in CFM. + * + * @param tenantName thhe name of the tenant. only used for display purposes + * @return the tenant ID. + */ + private String createTenant(String tenantName) { + return given() + .baseUri(Constants.TM_BASE_URL) + .contentType(Constants.APPLICATION_JSON) .body(""" { - "did": "%s", - "holderId": "%s", - "name": "%s tenant" - }""".formatted(participantContextDid, participantContextDid, participantContextId)) - .post("/issuer/admin/api/admin/v1alpha/participants/%s/holders".formatted(issuerIdBase64())) + "properties": { + "name": "%s", + "location": "eu" + } + } + """.formatted(tenantName)) + .post("/api/v1alpha1/tenants") .then() - .statusCode(201); + .log().ifValidationFails() + .statusCode(201) + .extract().body().jsonPath().getString("id"); } - private String issuerIdBase64() { - return Base64.getEncoder().encodeToString(issuerId.getBytes()); + + private void waitForCredentialIssuance(String participantContextId, String userToken, String holderPid) { + await().atMost(20, SECONDS) + .pollInterval(1, SECONDS).until(() -> { + var pcB64 = Base64.getUrlEncoder().encodeToString(participantContextId.getBytes()); + var response = given() + .baseUri(BASE_URL) + .contentType("application/json") + .auth().oauth2(userToken) + .get("/cs/api/identity/v1alpha/participants/%s/credentials/request/%s".formatted(pcB64, holderPid)) + .then() + .log().ifValidationFails() + .statusCode(200) + .extract() + .body().jsonPath().getString("status"); + return "ISSUED".equals(response); + }); } } diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CredentialListResponse.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/ClientCredentials.java similarity index 85% rename from tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CredentialListResponse.java rename to tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/ClientCredentials.java index 2b51ff6..b903723 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/CredentialListResponse.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/ClientCredentials.java @@ -14,5 +14,5 @@ package org.eclipse.edc.jad.tests.model; -public class CredentialListResponse { +public record ClientCredentials(String clientId, String clientSecret) { } diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/Orchestration.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/Orchestration.java new file mode 100644 index 0000000..6279313 --- /dev/null +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/model/Orchestration.java @@ -0,0 +1,247 @@ +/* + * 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.JsonProperty; + +import java.util.List; +import java.util.Map; + +public class Orchestration { + private String id; + private String correlationId; + private int state; + private String stateTimestamp; + private String createdTimestamp; + private String orchestrationType; + private List steps; + private ProcessingData processingData; + private Map outputData; + private Map completed; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + public String getStateTimestamp() { + return stateTimestamp; + } + + public void setStateTimestamp(String stateTimestamp) { + this.stateTimestamp = stateTimestamp; + } + + public String getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(String createdTimestamp) { + this.createdTimestamp = createdTimestamp; + } + + public String getOrchestrationType() { + return orchestrationType; + } + + public void setOrchestrationType(String orchestrationType) { + this.orchestrationType = orchestrationType; + } + + public List getSteps() { + return steps; + } + + public void setSteps(List steps) { + this.steps = steps; + } + + public ProcessingData getProcessingData() { + return processingData; + } + + public void setProcessingData(ProcessingData processingData) { + this.processingData = processingData; + } + + public Map getOutputData() { + return outputData; + } + + public void setOutputData(Map outputData) { + this.outputData = outputData; + } + + public Map getCompleted() { + return completed; + } + + public void setCompleted(Map completed) { + this.completed = completed; + } + + public static class Step { + private List activities; + + public List getActivities() { + return activities; + } + + public void setActivities(List activities) { + this.activities = activities; + } + } + + public static class Activity { + private String id; + private String type; + private String discriminator; + private List dependsOn; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDiscriminator() { + return discriminator; + } + + public void setDiscriminator(String discriminator) { + this.discriminator = discriminator; + } + + public List getDependsOn() { + return dependsOn; + } + + public void setDependsOn(List dependsOn) { + this.dependsOn = dependsOn; + } + } + + public static class ProcessingData { + @JsonProperty("cfm.participant.id") + private String participantId; + @JsonProperty("cfm.vpa.data") + private List vpaData; + @JsonProperty("clientID.apiAccess") + private String clientIdApiAccess; + @JsonProperty("clientID.vaultAccess") + private String clientIdVaultAccess; + + public String getParticipantId() { + return participantId; + } + + public void setParticipantId(String participantId) { + this.participantId = participantId; + } + + public List getVpaData() { + return vpaData; + } + + public void setVpaData(List vpaData) { + this.vpaData = vpaData; + } + + public String getClientIdApiAccess() { + return clientIdApiAccess; + } + + public void setClientIdApiAccess(String clientIdApiAccess) { + this.clientIdApiAccess = clientIdApiAccess; + } + + public String getClientIdVaultAccess() { + return clientIdVaultAccess; + } + + public void setClientIdVaultAccess(String clientIdVaultAccess) { + this.clientIdVaultAccess = clientIdVaultAccess; + } + } + + public static class VpaData { + private String cellId; + private String externalCellId; + private String id; + private String vpaType; + + public String getCellId() { + return cellId; + } + + public void setCellId(String cellId) { + this.cellId = cellId; + } + + public String getExternalCellId() { + return externalCellId; + } + + public void setExternalCellId(String externalCellId) { + this.externalCellId = externalCellId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getVpaType() { + return vpaType; + } + + public void setVpaType(String vpaType) { + this.vpaType = vpaType; + } + } + +}