From 8ac5fe159aa9dd5f6c6fa351c4bbf214017f4954 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 25 Mar 2026 16:33:01 +0100 Subject: [PATCH 01/22] cut the fat --- .run/Connector Consumer Corp.run.xml | 25 -- ...Connector _provider-manufacturing_.run.xml | 25 -- .run/Connector _provider-qna_.run.xml | 24 -- .run/IdentityHub Consumer Corp.run.xml | 25 -- .run/IdentityHub Provider Corp.run.xml | 24 -- .run/IssuerService.run.xml | 25 -- .run/Provider Catalog Server.run.xml | 25 -- .run/dataspace.run.xml | 12 - .run/remote/Catalog Server PROVIDER.run.xml | 16 -- .run/remote/Connector .run.xml | 16 -- .run/remote/IdentityHub CONSUMER.run.xml | 15 -- .run/remote/IdentityHub PROVIDER.run.xml | 15 -- build.gradle.kts | 4 +- .../catalog-node-resolver/build.gradle.kts | 27 --- .../ParticipantsResolverExtension.java | 85 ------- .../resolver/LazyLoadNodeDirectory.java | 91 ------- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 -- .../data-plane-public-api-v2/build.gradle.kts | 40 ++++ .../api/DataPlanePublicApiV2Extension.java | 113 +++++++++ .../ContainerRequestContextApi.java | 68 ++++++ .../ContainerRequestContextApiImpl.java | 106 ++++++++ .../controller/DataFlowRequestSupplier.java | 73 ++++++ .../api/controller/DataPlanePublicApiV2.java | 90 +++++++ .../DataPlanePublicApiV2Controller.java | 184 ++++++++++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../main/resources/public-api-version.json | 8 + .../DataFlowStartMessageSupplierTest.java | 94 ++++++++ .../DataPlanePublicApiV2ControllerTest.java | 226 ++++++++++++++++++ extensions/dcp-impl/build.gradle.kts | 27 --- .../DataAccessCredentialScopeExtractor.java | 38 --- .../edc/demo/dcp/core/DcpPatchExtension.java | 86 ------- .../dcp/core/DefaultScopeMappingFunction.java | 41 ---- .../AbstractCredentialEvaluationFunction.java | 42 ---- .../dcp/policy/DataAccessLevelFunction.java | 69 ------ ...embershipCredentialEvaluationFunction.java | 74 ------ .../dcp/policy/PolicyEvaluationExtension.java | 75 ------ .../edc/demo/dcp/policy/PolicyScopes.java | 28 --- ...rg.eclipse.edc.spi.system.ServiceExtension | 16 -- .../did-example-resolver/build.gradle.kts | 22 -- .../identitytrust/core/SecretsExtension.java | 65 ----- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 -- extensions/superuser-seed/build.gradle.kts | 24 -- .../seed/ParticipantContextSeedExtension.java | 99 -------- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 -- .../ParticipantContextSeedExtensionTest.java | 163 ------------- gradle.properties | 2 +- gradle/libs.versions.toml | 29 ++- .../application/controlplane-config.yaml | 68 ++++++ k8s/consumer/application/controlplane.yaml | 112 +++++++++ .../application/identityhub-config.yaml | 55 +++++ k8s/consumer/application/identityhub.yaml | 128 ++++++++++ k8s/consumer/base/gateway-class.yaml | 20 ++ k8s/consumer/base/gateway.yaml | 31 +++ k8s/consumer/base/namespace.yaml | 17 ++ k8s/consumer/base/postgres.yaml | 83 +++++++ k8s/consumer/base/vault.yaml | 80 +++++++ k8s/consumer/kustomization.yml | 23 ++ launchers/catalog-server/build.gradle.kts | 57 ----- .../catalog-server/src/main/docker/Dockerfile | 26 -- launchers/controlplane/build.gradle.kts | 11 +- .../controlplane/src/main/docker/Dockerfile | 3 - launchers/dataplane/build.gradle.kts | 15 +- .../dataplane/src/main/docker/Dockerfile | 3 - launchers/identity-hub/build.gradle.kts | 13 +- .../identity-hub/src/main/docker/Dockerfile | 3 - .../edc/demo/dcp/ih/IdentityHubExtension.java | 85 ------- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 -- launchers/issuerservice/build.gradle.kts | 8 +- .../issuerservice/src/main/docker/Dockerfile | 3 - launchers/runtime-embedded/build.gradle.kts | 44 ---- seed.sh | 182 -------------- settings.gradle.kts | 9 +- tests/end2end/build.gradle.kts | 3 - .../tests/transfer/TransferEndToEndTest.java | 12 +- values.yaml | 46 ++++ 75 files changed, 1707 insertions(+), 1850 deletions(-) delete mode 100644 .run/Connector Consumer Corp.run.xml delete mode 100644 .run/Connector _provider-manufacturing_.run.xml delete mode 100644 .run/Connector _provider-qna_.run.xml delete mode 100644 .run/IdentityHub Consumer Corp.run.xml delete mode 100644 .run/IdentityHub Provider Corp.run.xml delete mode 100644 .run/IssuerService.run.xml delete mode 100644 .run/Provider Catalog Server.run.xml delete mode 100644 .run/dataspace.run.xml delete mode 100644 .run/remote/Catalog Server PROVIDER.run.xml delete mode 100644 .run/remote/Connector .run.xml delete mode 100644 .run/remote/IdentityHub CONSUMER.run.xml delete mode 100644 .run/remote/IdentityHub PROVIDER.run.xml delete mode 100644 extensions/catalog-node-resolver/build.gradle.kts delete mode 100644 extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/ParticipantsResolverExtension.java delete mode 100644 extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/resolver/LazyLoadNodeDirectory.java delete mode 100644 extensions/catalog-node-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/data-plane-public-api-v2/build.gradle.kts create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/DataPlanePublicApiV2Extension.java create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApi.java create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApiImpl.java create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowRequestSupplier.java create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2.java create mode 100644 extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2Controller.java create mode 100644 extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/data-plane-public-api-v2/src/main/resources/public-api-version.json create mode 100644 extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowStartMessageSupplierTest.java create mode 100644 extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2ControllerTest.java delete mode 100644 extensions/dcp-impl/build.gradle.kts delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DefaultScopeMappingFunction.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/AbstractCredentialEvaluationFunction.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/DataAccessLevelFunction.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java delete mode 100644 extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyScopes.java delete mode 100644 extensions/dcp-impl/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 extensions/did-example-resolver/build.gradle.kts delete mode 100644 extensions/did-example-resolver/src/main/java/org/eclipse/edc/iam/identitytrust/core/SecretsExtension.java delete mode 100644 extensions/did-example-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 extensions/superuser-seed/build.gradle.kts delete mode 100644 extensions/superuser-seed/src/main/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtension.java delete mode 100644 extensions/superuser-seed/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 extensions/superuser-seed/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java create mode 100644 k8s/consumer/application/controlplane-config.yaml create mode 100644 k8s/consumer/application/controlplane.yaml create mode 100644 k8s/consumer/application/identityhub-config.yaml create mode 100644 k8s/consumer/application/identityhub.yaml create mode 100644 k8s/consumer/base/gateway-class.yaml create mode 100644 k8s/consumer/base/gateway.yaml create mode 100644 k8s/consumer/base/namespace.yaml create mode 100644 k8s/consumer/base/postgres.yaml create mode 100644 k8s/consumer/base/vault.yaml create mode 100644 k8s/consumer/kustomization.yml delete mode 100644 launchers/catalog-server/build.gradle.kts delete mode 100644 launchers/catalog-server/src/main/docker/Dockerfile delete mode 100644 launchers/identity-hub/src/main/java/org/eclipse/edc/demo/dcp/ih/IdentityHubExtension.java delete mode 100644 launchers/identity-hub/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension delete mode 100644 launchers/runtime-embedded/build.gradle.kts delete mode 100755 seed.sh create mode 100644 values.yaml diff --git a/.run/Connector Consumer Corp.run.xml b/.run/Connector Consumer Corp.run.xml deleted file mode 100644 index 8267531f0..000000000 --- a/.run/Connector Consumer Corp.run.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/Connector _provider-manufacturing_.run.xml b/.run/Connector _provider-manufacturing_.run.xml deleted file mode 100644 index 6f6e95af6..000000000 --- a/.run/Connector _provider-manufacturing_.run.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/Connector _provider-qna_.run.xml b/.run/Connector _provider-qna_.run.xml deleted file mode 100644 index e0884a6dd..000000000 --- a/.run/Connector _provider-qna_.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/IdentityHub Consumer Corp.run.xml b/.run/IdentityHub Consumer Corp.run.xml deleted file mode 100644 index 86896c70b..000000000 --- a/.run/IdentityHub Consumer Corp.run.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/IdentityHub Provider Corp.run.xml b/.run/IdentityHub Provider Corp.run.xml deleted file mode 100644 index 2fd35b8b5..000000000 --- a/.run/IdentityHub Provider Corp.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/IssuerService.run.xml b/.run/IssuerService.run.xml deleted file mode 100644 index ec950ca19..000000000 --- a/.run/IssuerService.run.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/Provider Catalog Server.run.xml b/.run/Provider Catalog Server.run.xml deleted file mode 100644 index 0a8ea2688..000000000 --- a/.run/Provider Catalog Server.run.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/dataspace.run.xml b/.run/dataspace.run.xml deleted file mode 100644 index 07a8593da..000000000 --- a/.run/dataspace.run.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.run/remote/Catalog Server PROVIDER.run.xml b/.run/remote/Catalog Server PROVIDER.run.xml deleted file mode 100644 index 42ec8dd1b..000000000 --- a/.run/remote/Catalog Server PROVIDER.run.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/remote/Connector .run.xml b/.run/remote/Connector .run.xml deleted file mode 100644 index 08726f829..000000000 --- a/.run/remote/Connector .run.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/remote/IdentityHub CONSUMER.run.xml b/.run/remote/IdentityHub CONSUMER.run.xml deleted file mode 100644 index 22be6d2c7..000000000 --- a/.run/remote/IdentityHub CONSUMER.run.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/.run/remote/IdentityHub PROVIDER.run.xml b/.run/remote/IdentityHub PROVIDER.run.xml deleted file mode 100644 index 3fbbad1b3..000000000 --- a/.run/remote/IdentityHub PROVIDER.run.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ae06123f4..eb529ac92 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,8 +39,8 @@ subprojects { tasks.register("dockerize", DockerBuildImage::class) { val dockerContextDir = project.projectDir dockerFile.set(file("$dockerContextDir/src/main/docker/Dockerfile")) - images.add("${project.name}:${project.version}") - images.add("${project.name}:latest") + images.add("ghcr.io/eclipse-edc/mvd/${project.name}:${project.version}") + images.add("ghcr.io/eclipse-edc/mvd/${project.name}:latest") // specify platform with the -Dplatform flag: if (System.getProperty("platform") != null) platform.set(System.getProperty("platform")) diff --git a/extensions/catalog-node-resolver/build.gradle.kts b/extensions/catalog-node-resolver/build.gradle.kts deleted file mode 100644 index 5d2be6faa..000000000 --- a/extensions/catalog-node-resolver/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -/* -* 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-library` -} - -dependencies { - implementation(libs.edc.spi.identity.did) - implementation(libs.edc.fc.spi.crawler) - runtimeOnly(libs.edc.fc.core) - // todo: use 2025 once it is used everywhere - // runtimeOnly(libs.edc.fc.core2025) - runtimeOnly(libs.edc.fc.core08) - runtimeOnly(libs.edc.fc.api) -} diff --git a/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/ParticipantsResolverExtension.java b/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/ParticipantsResolverExtension.java deleted file mode 100644 index c31ec3918..000000000 --- a/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/ParticipantsResolverExtension.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.participants; - -import org.eclipse.edc.crawler.spi.TargetNodeDirectory; -import org.eclipse.edc.crawler.spi.TargetNodeFilter; -import org.eclipse.edc.demo.participants.resolver.LazyLoadNodeDirectory; -import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry; -import org.eclipse.edc.runtime.metamodel.annotation.Extension; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.runtime.metamodel.annotation.Provider; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.types.TypeManager; - -import java.io.File; - -import static org.eclipse.edc.demo.participants.ParticipantsResolverExtension.NAME; - -@Extension(value = NAME) -public class ParticipantsResolverExtension implements ServiceExtension { - public static final String NAME = "MVD Participant Resolver Extension"; - - public static final String PARTICIPANT_LIST_FILE_PATH = "edc.mvd.participants.list.file"; - - @Inject - private TypeManager typeManager; - - @Inject - private DidResolverRegistry didResolverRegistry; - - private File participantListFile; - private Monitor monitor; - private TargetNodeDirectory nodeDirectory; - - @Override - public String name() { - return NAME; - } - - @Override - public void initialize(ServiceExtensionContext context) { - var participantsPath = context.getConfig().getString(PARTICIPANT_LIST_FILE_PATH); - monitor = context.getMonitor().withPrefix("DEMO"); - - participantListFile = new File(participantsPath).getAbsoluteFile(); - if (!participantListFile.exists()) { - monitor.warning("Path '%s' does not exist. It must be a resolvable path with read access. Will not add any VCs.".formatted(participantsPath)); - } - } - - @Provider - public TargetNodeDirectory createLazyTargetNodeDirectory() { - if (nodeDirectory == null) { - nodeDirectory = new LazyLoadNodeDirectory(typeManager.getMapper(), participantListFile, didResolverRegistry, monitor); - } - return nodeDirectory; - } - - @Provider - public TargetNodeFilter skipSelfNodeFilter(ServiceExtensionContext context) { - return targetNode -> { - var predicateTest = !targetNode.id().equals(context.getParticipantId()); - if (!predicateTest) { - monitor.debug("Node filter: skipping node '%s' for participant '%s'".formatted(targetNode.id(), context.getParticipantId())); - } - return predicateTest; - }; - } - - -} diff --git a/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/resolver/LazyLoadNodeDirectory.java b/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/resolver/LazyLoadNodeDirectory.java deleted file mode 100644 index 2330467c6..000000000 --- a/extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/resolver/LazyLoadNodeDirectory.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.participants.resolver; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.eclipse.edc.crawler.spi.TargetNode; -import org.eclipse.edc.crawler.spi.TargetNodeDirectory; -import org.eclipse.edc.iam.did.spi.document.Service; -import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * {@link TargetNodeDirectory} that is initialized with a file, that contains participant DIDs. On the first getAll() request - * the DIDs are resolved and converted into {@link TargetNode} objects. From then on, they are held in memory and cached. - *

- * DIDs must contain a {@link org.eclipse.edc.iam.did.spi.document.Service} where the {@link Service#getType()} equals {@code ProtocolEndpoint} - */ -public class LazyLoadNodeDirectory implements TargetNodeDirectory { - private static final TypeReference> MAP_TYPE = new TypeReference<>() { - }; - private static final String PROTOCOL_ENDPOINT = "ProtocolEndpoint"; - - private final ObjectMapper mapper; - private final File participantListFile; - private final DidResolverRegistry didResolverRegistry; - private final Monitor monitor; - - public LazyLoadNodeDirectory(ObjectMapper mapper, File participantListFile, DidResolverRegistry didResolverRegistry, Monitor monitor) { - - this.mapper = mapper; - this.participantListFile = participantListFile; - this.didResolverRegistry = didResolverRegistry; - this.monitor = monitor; - } - - @Override - public List getAll() { - try { - var entries = mapper.readValue(participantListFile, MAP_TYPE); - - return entries.entrySet().stream() - .map(e -> createNode(e.getKey(), e.getValue())) - .filter(Objects::nonNull) - .toList(); - } catch (IOException e) { - throw new EdcException(e); - } - } - - @Override - public void insert(TargetNode targetNode) { - //noop - } - - @Override - public TargetNode remove(String s) { - return null; - } - - private TargetNode createNode(String name, String did) { - var didResult = didResolverRegistry.resolve(did); - if (didResult.failed()) { - monitor.warning(didResult.getFailureDetail()); - return null; - } - var document = didResult.getContent(); - var service = document.getService().stream().filter(s -> s.getType().equalsIgnoreCase(PROTOCOL_ENDPOINT)).findFirst(); - return service.map(s -> new TargetNode(name, did, s.getServiceEndpoint(), List.of("dataspace-protocol-http"))) - .orElse(null); - } -} diff --git a/extensions/catalog-node-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/catalog-node-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index 9b6064c26..000000000 --- a/extensions/catalog-node-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright (c) 2024 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 -# -# - -org.eclipse.edc.demo.participants.ParticipantsResolverExtension \ No newline at end of file diff --git a/extensions/data-plane-public-api-v2/build.gradle.kts b/extensions/data-plane-public-api-v2/build.gradle.kts new file mode 100644 index 000000000..78db2ebea --- /dev/null +++ b/extensions/data-plane-public-api-v2/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * 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 + * + */ + +plugins { + `java-library` + id(libs.plugins.swagger.get().pluginId) +} + +dependencies { + api(libs.edc.spi.http) + api(libs.edc.spi.web) + api(libs.edc.spi.dataplane) + implementation(libs.edc.lib.util) + implementation(libs.edc.lib.util.dataplane) + implementation(libs.jakarta.rsApi) + + testImplementation(libs.edc.lib.http) + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(testFixtures(libs.edc.core.jersey)) + +} +edcBuild { + swagger { + apiGroup.set("public-api") + } +} + + diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/DataPlanePublicApiV2Extension.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/DataPlanePublicApiV2Extension.java new file mode 100644 index 000000000..612e0256c --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/DataPlanePublicApiV2Extension.java @@ -0,0 +1,113 @@ +/* + * 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.connector.dataplane.api; + +import org.eclipse.edc.connector.dataplane.api.controller.DataPlanePublicApiV2Controller; +import org.eclipse.edc.connector.dataplane.spi.Endpoint; +import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService; +import org.eclipse.edc.connector.dataplane.spi.iam.PublicEndpointGeneratorService; +import org.eclipse.edc.connector.dataplane.spi.pipeline.PipelineService; +import org.eclipse.edc.runtime.metamodel.annotation.Configuration; +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.runtime.metamodel.annotation.Settings; +import org.eclipse.edc.spi.system.ExecutorInstrumentation; +import org.eclipse.edc.spi.system.Hostname; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.PortMapping; +import org.eclipse.edc.web.spi.configuration.PortMappingRegistry; + +import java.util.concurrent.Executors; + +/** + * This extension provides generic endpoints which are open to public participants of the Dataspace to execute + * requests on the actual data source. + */ +@Extension(value = DataPlanePublicApiV2Extension.NAME) +public class DataPlanePublicApiV2Extension implements ServiceExtension { + public static final String NAME = "Data Plane Public API"; + + public static final String API_CONTEXT = "public"; + private static final int DEFAULT_PUBLIC_PORT = 8185; + private static final String DEFAULT_PUBLIC_PATH = "/api/public"; + private static final int DEFAULT_THREAD_POOL = 10; + @Setting(description = "Base url of the public API endpoint without the trailing slash. This should point to the public endpoint configured.", + required = false, + key = "edc.dataplane.api.public.baseurl", warnOnMissingConfig = true) + private String publicBaseUrl; + @Setting(description = "Optional base url of the response channel endpoint without the trailing slash. A common practice is to use /responseChannel", key = "edc.dataplane.api.public.response.baseurl", required = false) + private String publicApiResponseUrl; + @Configuration + private PublicApiConfiguration apiConfiguration; + @Inject + private PortMappingRegistry portMappingRegistry; + @Inject + private PipelineService pipelineService; + @Inject + private WebService webService; + @Inject + private ExecutorInstrumentation executorInstrumentation; + @Inject + private DataPlaneAuthorizationService authorizationService; + @Inject + private PublicEndpointGeneratorService generatorService; + @Inject + private Hostname hostname; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + context.getMonitor().warning("The `data-plane-public-api-v2` has been deprecated, please provide an" + + "alternative implementation for Http Proxy if needed"); + + var portMapping = new PortMapping(API_CONTEXT, apiConfiguration.port(), apiConfiguration.path()); + portMappingRegistry.register(portMapping); + var executorService = executorInstrumentation.instrument( + Executors.newFixedThreadPool(DEFAULT_THREAD_POOL), + "Data plane proxy transfers" + ); + + if (publicBaseUrl == null) { + publicBaseUrl = "http://%s:%d%s".formatted(hostname.get(), portMapping.port(), portMapping.path()); + context.getMonitor().warning("The public API endpoint was not explicitly configured, the default '%s' will be used.".formatted(publicBaseUrl)); + } + var endpoint = Endpoint.url(publicBaseUrl); + generatorService.addGeneratorFunction("HttpData", dataAddress -> endpoint); + + if (publicApiResponseUrl != null) { + generatorService.addResponseGeneratorFunction("HttpData", () -> Endpoint.url(publicApiResponseUrl)); + } + + var publicApiController = new DataPlanePublicApiV2Controller(pipelineService, executorService, authorizationService); + webService.registerResource(API_CONTEXT, publicApiController); + } + + @Settings + record PublicApiConfiguration( + @Setting(key = "web.http." + API_CONTEXT + ".port", description = "Port for " + API_CONTEXT + " api context", defaultValue = DEFAULT_PUBLIC_PORT + "") + int port, + @Setting(key = "web.http." + API_CONTEXT + ".path", description = "Path for " + API_CONTEXT + " api context", defaultValue = DEFAULT_PUBLIC_PATH) + String path + ) { + + } +} diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApi.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApi.java new file mode 100644 index 000000000..5bc605054 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApi.java @@ -0,0 +1,68 @@ +/* + * 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.connector.dataplane.api.controller; + +import jakarta.ws.rs.container.ContainerRequestContext; + +import java.util.Map; + +/** + * Wrapper around {@link ContainerRequestContext} enabling mocking. + */ +public interface ContainerRequestContextApi { + + /** + * Get the request headers. Note that if more than one value is associated to a specific header, + * only the first one is retained. + * + * @return Headers map. + */ + Map headers(); + + /** + * Format query of the request as string, e.g. "hello=world\&foo=bar". + * + * @return Query param string. + */ + String queryParams(); + + /** + * Format the request body into a string. + * + * @return Request body. + */ + String body(); + + /** + * Get the media type from incoming request. + * + * @return Media type. + */ + String mediaType(); + + /** + * Return request path, e.g. "hello/world/foo/bar". + * + * @return Path string. + */ + String path(); + + /** + * Get http method from the incoming request, e.g. "GET", "POST"... + * + * @return Http method. + */ + String method(); +} diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApiImpl.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApiImpl.java new file mode 100644 index 000000000..6bcb5ba6d --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/ContainerRequestContextApiImpl.java @@ -0,0 +1,106 @@ +/* + * 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.connector.dataplane.api.controller; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.edc.spi.EdcException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * This class provides a set of API wrapping a {@link ContainerRequestContext}. + */ +public class ContainerRequestContextApiImpl implements ContainerRequestContextApi { + + private static final String QUERY_PARAM_SEPARATOR = "&"; + + private final ContainerRequestContext context; + + public ContainerRequestContextApiImpl(ContainerRequestContext context) { + this.context = context; + } + + @Override + public Map headers() { + return context.getHeaders().entrySet() + .stream() + .filter(entry -> !entry.getValue().isEmpty()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get(0))); + } + + @Override + public String queryParams() { + return context.getUriInfo().getQueryParameters().entrySet() + .stream() + .flatMap(entry -> entry.getValue().stream().map(val -> new QueryParam(entry.getKey(), val))) + .map(QueryParam::toString) + .collect(Collectors.joining(QUERY_PARAM_SEPARATOR)); + } + + @Override + public String body() { + try (BufferedReader br = new BufferedReader(new InputStreamReader(context.getEntityStream()))) { + return br.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new EdcException("Failed to read request body: " + e.getMessage()); + } + } + + @Override + public String mediaType() { + return Optional.ofNullable(context.getMediaType()) + .map(MediaType::toString) + .orElse(null); + } + + @Override + public String path() { + var pathInfo = context.getUriInfo().getPath(); + return pathInfo.startsWith("/") ? pathInfo.substring(1) : pathInfo; + } + + @Override + public String method() { + return context.getMethod(); + } + + private static final class QueryParam { + + private final String key; + private final String values; + private final boolean valid; + + private QueryParam(String key, String values) { + this.key = key; + this.values = values; + this.valid = key != null && values != null && !values.isEmpty(); + } + + public boolean isValid() { + return valid; + } + + @Override + public String toString() { + return valid ? key + "=" + values : ""; + } + } +} diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowRequestSupplier.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowRequestSupplier.java new file mode 100644 index 000000000..c3c1aa7b9 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowRequestSupplier.java @@ -0,0 +1,73 @@ +/* + * 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.connector.dataplane.api.controller; + +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; +import org.eclipse.edc.spi.types.domain.transfer.FlowType; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; + +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.BODY; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.MEDIA_TYPE; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.METHOD; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.PATH; +import static org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema.QUERY_PARAMS; + +public class DataFlowRequestSupplier implements BiFunction { + + /** + * Put all properties of the incoming request (method, request body, query params...) into a map. + */ + private static Map createProps(ContainerRequestContextApi contextApi) { + var props = new HashMap(); + props.put(METHOD, contextApi.method()); + props.put(QUERY_PARAMS, contextApi.queryParams()); + props.put(PATH, contextApi.path()); + Optional.ofNullable(contextApi.mediaType()) + .ifPresent(mediaType -> { + props.put(MEDIA_TYPE, mediaType); + props.put(BODY, contextApi.body()); + }); + return props; + } + + /** + * Create a {@link DataFlowStartMessage} based on incoming request and claims decoded from the access token. + * + * @param contextApi Api for accessing request properties. + * @param dataAddress Source data address. + * @return DataFlowRequest + */ + @Override + public DataFlowStartMessage apply(ContainerRequestContextApi contextApi, DataAddress dataAddress) { + var props = createProps(contextApi); + return DataFlowStartMessage.Builder.newInstance() + .processId(UUID.randomUUID().toString()) + .sourceDataAddress(dataAddress) + .flowType(FlowType.PULL) // if a request hits the public DP API, we can assume a PULL transfer + .destinationDataAddress(DataAddress.Builder.newInstance() + .type(AsyncStreamingDataSink.TYPE) + .build()) + .id(UUID.randomUUID().toString()) + .properties(props) + .build(); + } +} diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2.java new file mode 100644 index 000000000..aea4942e4 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2.java @@ -0,0 +1,90 @@ +/* + * 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.connector.dataplane.api.controller; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.ContainerRequestContext; + +@OpenAPIDefinition +@Tag(name = "Data Plane public API", + description = "The public API of the Data Plane is a data proxy enabling a data consumer to actively query" + + "data from the provider data source (e.g. backend Rest API, internal database...) through its Data Plane" + + "instance. Thus the Data Plane is the only entry/output door for the data, which avoids the provider to expose" + + "directly its data externally." + + "The Data Plane public API being a proxy, it supports all verbs (i.e. GET, POST, PUT, PATCH, DELETE), which" + + "can then conveyed until the data source is required. This is especially useful when the actual data source" + + "is a Rest API itself." + + "In the same manner, any set of arbitrary query parameters, path parameters and request body are supported " + + "(in the limits fixed by the HTTP server) and can also conveyed to the actual data source.") +public interface DataPlanePublicApiV2 { + + @Operation(description = "Send `GET` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void get(ContainerRequestContext context, AsyncResponse response); + + @Operation(description = "Send `HEAD` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void head(ContainerRequestContext context, AsyncResponse response); + + @Operation(description = "Send `POST` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void post(ContainerRequestContext context, AsyncResponse response); + + @Operation(description = "Send `PUT` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void put(ContainerRequestContext context, AsyncResponse response); + + @Operation(description = "Send `DELETE` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void delete(ContainerRequestContext context, AsyncResponse response); + + @Operation(description = "Send `PATCH` data query to the Data Plane.", + responses = { + @ApiResponse(responseCode = "400", description = "Missing access token"), + @ApiResponse(responseCode = "403", description = "Access token is expired or invalid"), + @ApiResponse(responseCode = "500", description = "Failed to transfer data") + } + ) + void patch(ContainerRequestContext context, AsyncResponse response); +} diff --git a/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2Controller.java b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2Controller.java new file mode 100644 index 000000000..1502fe8a1 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2Controller.java @@ -0,0 +1,184 @@ +/* + * 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.connector.dataplane.api.controller; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService; +import org.eclipse.edc.connector.dataplane.spi.pipeline.PipelineService; +import org.eclipse.edc.connector.dataplane.spi.response.TransferErrorResponse; +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.WILDCARD; +import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; +import static jakarta.ws.rs.core.Response.status; + +@Path("{any:.*}") +@Produces(WILDCARD) +public class DataPlanePublicApiV2Controller implements DataPlanePublicApiV2 { + + private final PipelineService pipelineService; + private final DataFlowRequestSupplier requestSupplier; + private final ExecutorService executorService; + private final DataPlaneAuthorizationService authorizationService; + + public DataPlanePublicApiV2Controller(PipelineService pipelineService, + ExecutorService executorService, + DataPlaneAuthorizationService authorizationService) { + this.pipelineService = pipelineService; + this.authorizationService = authorizationService; + this.requestSupplier = new DataFlowRequestSupplier(); + this.executorService = executorService; + } + + private static Response error(Response.Status status, List errors) { + return status(status).type(APPLICATION_JSON).entity(new TransferErrorResponse(errors)).build(); + } + + @GET + @Override + public void get(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + @HEAD + @Override + public void head(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + /** + * Sends a {@link POST} request to the data source and returns data. + * + * @param requestContext Request context. + * @param response Data fetched from the data source. + */ + @POST + @Override + public void post(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + /** + * Sends a {@link PUT} request to the data source and returns data. + * + * @param requestContext Request context. + * @param response Data fetched from the data source. + */ + @PUT + @Override + public void put(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + /** + * Sends a {@link DELETE} request to the data source and returns data. + * + * @param requestContext Request context. + * @param response Data fetched from the data source. + */ + @DELETE + @Override + public void delete(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + /** + * Sends a {@link PATCH} request to the data source and returns data. + * + * @param requestContext Request context. + * @param response Data fetched from the data source. + */ + @PATCH + @Override + public void patch(@Context ContainerRequestContext requestContext, @Suspended AsyncResponse response) { + handle(requestContext, response); + } + + private void handle(ContainerRequestContext requestContext, AsyncResponse response) { + var contextApi = new ContainerRequestContextApiImpl(requestContext); + + var token = contextApi.headers().get(HttpHeaders.AUTHORIZATION); + if (token == null) { + response.resume(error(UNAUTHORIZED, List.of("Missing Authorization Header"))); + return; + } + + var sourceDataAddress = authorizationService.authorize(token, buildRequestData(requestContext)); + if (sourceDataAddress.failed()) { + response.resume(error(FORBIDDEN, sourceDataAddress.getFailureMessages())); + return; + } + + var startMessage = requestSupplier.apply(contextApi, sourceDataAddress.getContent()); + + processRequest(startMessage, response); + } + + private Map buildRequestData(ContainerRequestContext requestContext) { + var requestData = new HashMap(); + requestData.put("headers", requestContext.getHeaders()); + requestData.put("path", requestContext.getUriInfo()); + requestData.put("method", requestContext.getMethod()); + requestData.put("content-type", requestContext.getMediaType()); + return requestData; + } + + private void processRequest(DataFlowStartMessage dataFlowStartMessage, AsyncResponse response) { + + AsyncStreamingDataSink.AsyncResponseContext asyncResponseContext = callback -> { + StreamingOutput output = t -> callback.outputStreamConsumer().accept(t); + var resp = Response.ok(output).type(callback.mediaType()).build(); + return response.resume(resp); + }; + + var sink = new AsyncStreamingDataSink(asyncResponseContext, executorService); + + pipelineService.transfer(dataFlowStartMessage, sink) + .whenComplete((result, throwable) -> { + if (throwable == null) { + if (result.failed()) { + response.resume(error(INTERNAL_SERVER_ERROR, result.getFailureMessages())); + } + } else { + var error = "Unhandled exception occurred during data transfer: " + throwable.getMessage(); + response.resume(error(INTERNAL_SERVER_ERROR, List.of(error))); + } + }); + } + +} diff --git a/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..432c0528d --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.eclipse.edc.connector.dataplane.api.DataPlanePublicApiV2Extension diff --git a/extensions/data-plane-public-api-v2/src/main/resources/public-api-version.json b/extensions/data-plane-public-api-v2/src/main/resources/public-api-version.json new file mode 100644 index 000000000..72890702c --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/main/resources/public-api-version.json @@ -0,0 +1,8 @@ +[ + { + "version": "2.0.1", + "urlPath": "/v2", + "lastUpdated": "2024-07-10T08:56:00Z", + "maturity": "stable" + } +] diff --git a/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowStartMessageSupplierTest.java b/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowStartMessageSupplierTest.java new file mode 100644 index 000000000..8961e2e42 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataFlowStartMessageSupplierTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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: + * Amadeus - initial API and implementation + * + */ + +package org.eclipse.edc.connector.dataplane.api.controller; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.edc.connector.dataplane.spi.schema.DataFlowRequestSchema; +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DataFlowStartMessageSupplierTest { + + + private final DataFlowRequestSupplier supplier = new DataFlowRequestSupplier(); + + private static DataAddress createDataAddress() { + return DataAddress.Builder.newInstance().type("test-type").build(); + } + + @Test + void verifyMapping_noInputBody() { + var contextApi = mock(ContainerRequestContextApi.class); + var address = createDataAddress(); + + var method = HttpMethod.GET; + var queryParams = "test-query-param"; + var path = "test-path"; + + when(contextApi.method()).thenReturn(method); + when(contextApi.queryParams()).thenReturn(queryParams); + when(contextApi.path()).thenReturn(path); + + var request = supplier.apply(contextApi, address); + + assertThat(request.getId()).isNotBlank(); + assertThat(request.getDestinationDataAddress().getType()).isEqualTo(AsyncStreamingDataSink.TYPE); + assertThat(request.getSourceDataAddress().getType()).isEqualTo(address.getType()); + assertThat(request.getProperties()).containsExactlyInAnyOrderEntriesOf(Map.of( + DataFlowRequestSchema.PATH, path, + DataFlowRequestSchema.METHOD, method, + DataFlowRequestSchema.QUERY_PARAMS, queryParams + + )); + } + + @Test + void verifyMapping_withInputBody() { + var contextApi = mock(ContainerRequestContextApi.class); + var address = createDataAddress(); + + var method = HttpMethod.GET; + var queryParams = "test-query-param"; + var path = "test-path"; + var body = "Test request body"; + + when(contextApi.method()).thenReturn(method); + when(contextApi.queryParams()).thenReturn(queryParams); + when(contextApi.path()).thenReturn(path); + when(contextApi.mediaType()).thenReturn(MediaType.TEXT_PLAIN); + when(contextApi.body()).thenReturn(body); + + var request = supplier.apply(contextApi, address); + + assertThat(request.getId()).isNotBlank(); + assertThat(request.getDestinationDataAddress().getType()).isEqualTo(AsyncStreamingDataSink.TYPE); + assertThat(request.getSourceDataAddress().getType()).isEqualTo(address.getType()); + assertThat(request.getProperties()).containsExactlyInAnyOrderEntriesOf(Map.of( + DataFlowRequestSchema.PATH, path, + DataFlowRequestSchema.METHOD, method, + DataFlowRequestSchema.QUERY_PARAMS, queryParams, + DataFlowRequestSchema.BODY, body, + DataFlowRequestSchema.MEDIA_TYPE, MediaType.TEXT_PLAIN + )); + } +} diff --git a/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2ControllerTest.java b/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2ControllerTest.java new file mode 100644 index 000000000..05aba40c3 --- /dev/null +++ b/extensions/data-plane-public-api-v2/src/test/java/org/eclipse/edc/connector/dataplane/api/controller/DataPlanePublicApiV2ControllerTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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: + * Amadeus - initial API and implementation + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package org.eclipse.edc.connector.dataplane.api.controller; + +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService; +import org.eclipse.edc.connector.dataplane.spi.pipeline.DataSource; +import org.eclipse.edc.connector.dataplane.spi.pipeline.PipelineService; +import org.eclipse.edc.connector.dataplane.spi.pipeline.StreamFailure; +import org.eclipse.edc.connector.dataplane.spi.pipeline.StreamResult; +import org.eclipse.edc.connector.dataplane.spi.resolver.DataAddressResolver; +import org.eclipse.edc.connector.dataplane.util.sink.AsyncStreamingDataSink; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ApiTest +class DataPlanePublicApiV2ControllerTest extends RestControllerTestBase { + + private final PipelineService pipelineService = mock(); + private final DataAddressResolver dataAddressResolver = mock(); + private final DataPlaneAuthorizationService authorizationService = mock(); + + @BeforeEach + void setup() { + when(authorizationService.authorize(anyString(), anyMap())) + .thenReturn(Result.success(testDestAddress())); + } + + @Test + void should_returnBadRequest_if_missingAuthorizationHeader() { + baseRequest() + .post("/any") + .then() + .statusCode(Response.Status.UNAUTHORIZED.getStatusCode()) + .body("errors[0]", is("Missing Authorization Header")); + } + + @Test + void shouldNotReturn302_whenUrlWithoutTrailingSlash() { + baseRequest() + .post("") + .then() + .statusCode(not(302)); + } + + @Test + void should_returnForbidden_if_tokenValidationFails() { + var token = UUID.randomUUID().toString(); + when(authorizationService.authorize(anyString(), anyMap())).thenReturn(Result.failure("token is not valid")); + + baseRequest() + .header(AUTHORIZATION, token) + .post("/any") + .then() + .statusCode(Response.Status.FORBIDDEN.getStatusCode()) + .contentType(JSON) + .body("errors.size()", is(1)); + + verify(authorizationService).authorize(eq(token), anyMap()); + } + + @Test + void should_returnInternalServerError_if_transferFails() { + var token = UUID.randomUUID().toString(); + var errorMsg = UUID.randomUUID().toString(); + when(dataAddressResolver.resolve(any())).thenReturn(Result.success(testDestAddress())); + when(pipelineService.transfer(any(), any())) + .thenReturn(completedFuture(StreamResult.error(errorMsg))); + + baseRequest() + .header(AUTHORIZATION, token) + .when() + .post("/any") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) + .contentType(JSON) + .body("errors[0]", is(errorMsg)); + } + + @Test + void should_returnListOfErrorsAsResponse_if_anythingFails() { + var token = UUID.randomUUID().toString(); + var firstErrorMsg = UUID.randomUUID().toString(); + var secondErrorMsg = UUID.randomUUID().toString(); + + when(dataAddressResolver.resolve(any())).thenReturn(Result.success(testDestAddress())); + when(pipelineService.transfer(any(), any())) + .thenReturn(completedFuture(StreamResult.failure(new StreamFailure(List.of(firstErrorMsg, secondErrorMsg), StreamFailure.Reason.GENERAL_ERROR)))); + + baseRequest() + .header(AUTHORIZATION, token) + .when() + .post("/any") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) + .contentType(JSON) + .body("errors", isA(List.class)) + .body("errors[0]", is(firstErrorMsg)) + .body("errors[1]", is(secondErrorMsg)); + } + + @Test + void should_returnInternalServerError_if_transferThrows() { + var token = UUID.randomUUID().toString(); + var errorMsg = UUID.randomUUID().toString(); + when(dataAddressResolver.resolve(any())).thenReturn(Result.success(testDestAddress())); + when(pipelineService.transfer(any(DataFlowStartMessage.class), any())) + .thenReturn(failedFuture(new RuntimeException(errorMsg))); + + baseRequest() + .header(AUTHORIZATION, token) + .when() + .post("/any") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) + .contentType(JSON) + .body("errors[0]", is("Unhandled exception occurred during data transfer: " + errorMsg)); + } + + @Test + void shouldStreamSourceToResponse() { + when(dataAddressResolver.resolve(any())).thenReturn(Result.success(testDestAddress())); + when(pipelineService.transfer(any(), any())).thenAnswer(i -> { + ((AsyncStreamingDataSink) i.getArgument(1)).transfer(new TestDataSource("application/something", "data")); + return CompletableFuture.completedFuture(StreamResult.success()); + }); + + var responseBody = baseRequest() + .header(AUTHORIZATION, UUID.randomUUID().toString()) + .when() + .post("/any?foo=bar") + .then() + .log().ifError() + .statusCode(Response.Status.OK.getStatusCode()) + .contentType("application/something") + .extract().body().asString(); + + assertThat(responseBody).isEqualTo("data"); + var requestCaptor = ArgumentCaptor.forClass(DataFlowStartMessage.class); + verify(pipelineService).transfer(requestCaptor.capture(), any()); + var request = requestCaptor.getValue(); + assertThat(request.getDestinationDataAddress().getType()).isEqualTo(AsyncStreamingDataSink.TYPE); + assertThat(request.getSourceDataAddress().getType()).isEqualTo("test"); + assertThat(request.getProperties()).containsEntry("method", "POST").containsEntry("pathSegments", "any").containsEntry("queryParams", "foo=bar"); + } + + @Override + protected Object controller() { + return new DataPlanePublicApiV2Controller(pipelineService, Executors.newSingleThreadExecutor(), authorizationService); + } + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port) + .when(); + } + + private DataAddress testDestAddress() { + return DataAddress.Builder.newInstance().type("test").build(); + } + + private record TestDataSource(String mediaType, String data) implements DataSource, DataSource.Part { + + @Override + public StreamResult> openPartStream() { + return StreamResult.success(Stream.of(this)); + } + + @Override + public String name() { + return "test"; + } + + @Override + public InputStream openStream() { + return new ByteArrayInputStream(data.getBytes()); + } + + } + +} diff --git a/extensions/dcp-impl/build.gradle.kts b/extensions/dcp-impl/build.gradle.kts deleted file mode 100644 index 87863004d..000000000 --- a/extensions/dcp-impl/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -/* -* 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-library` -} - -dependencies { - implementation(libs.edc.dcp.core) - implementation(libs.edc.spi.identity.trust) - implementation(libs.edc.spi.transform) - implementation(libs.edc.spi.catalog) - implementation(libs.edc.spi.identity.did) - implementation(libs.edc.lib.jws2020) - implementation(libs.edc.lib.transform) -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java deleted file mode 100644 index 46b192f37..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.core; - -import org.eclipse.edc.iam.identitytrust.spi.scope.ScopeExtractor; -import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; -import org.eclipse.edc.policy.model.Operator; - -import java.util.Set; - -class DataAccessCredentialScopeExtractor implements ScopeExtractor { - public static final String DATA_PROCESSOR_CREDENTIAL_TYPE = "DataProcessorCredential"; - private static final String DATA_ACCESS_CONSTRAINT_PREFIX = "DataAccess."; - private static final String CREDENTIAL_TYPE_NAMESPACE = "org.eclipse.edc.vc.type"; - - @Override - public Set extractScopes(Object leftValue, Operator operator, Object rightValue, RequestPolicyContext context) { - Set scopes = Set.of(); - if (leftValue instanceof String leftOperand) { - if (leftOperand.startsWith(DATA_ACCESS_CONSTRAINT_PREFIX)) { - scopes = Set.of("%s:%s:read".formatted(CREDENTIAL_TYPE_NAMESPACE, DATA_PROCESSOR_CREDENTIAL_TYPE)); - } - } - return scopes; - } -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java deleted file mode 100644 index 2f4d8f46a..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.dcp.core; - -import org.eclipse.edc.iam.identitytrust.spi.scope.ScopeExtractorRegistry; -import org.eclipse.edc.iam.identitytrust.spi.verification.SignatureSuiteRegistry; -import org.eclipse.edc.iam.verifiablecredentials.spi.VcConstants; -import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; -import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry; -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.context.request.spi.RequestVersionPolicyContext; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.security.signature.jws2020.Jws2020SignatureSuite; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.types.TypeManager; -import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; - -import java.util.Map; -import java.util.Set; - -import static org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry.WILDCARD; -import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; - -public class DcpPatchExtension implements ServiceExtension { - @Inject - private TypeManager typeManager; - - @Inject - private PolicyEngine policyEngine; - - @Inject - private SignatureSuiteRegistry signatureSuiteRegistry; - - @Inject - private TrustedIssuerRegistry trustedIssuerRegistry; - - @Inject - private ScopeExtractorRegistry scopeExtractorRegistry; - @Inject - private TypeTransformerRegistry typeTransformerRegistry; - - @Override - public void initialize(ServiceExtensionContext context) { - - // register signature suite - var suite = new Jws2020SignatureSuite(typeManager.getMapper(JSON_LD)); - signatureSuiteRegistry.register(VcConstants.JWS_2020_SIGNATURE_SUITE, suite); - - // register dataspace issuer - trustedIssuerRegistry.register(new Issuer("did:web:dataspace-issuer", Map.of()), WILDCARD); - trustedIssuerRegistry.register(new Issuer("did:web:localhost%3A9876", Map.of()), WILDCARD); // for the standard credentials - trustedIssuerRegistry.register(new Issuer("did:web:localhost%3A10100", Map.of()), WILDCARD); // for the credential used to demo the issuance flow - - // register a default scope provider - var contextMappingFunction = new DefaultScopeMappingFunction(Set.of("org.eclipse.edc.vc.type:MembershipCredential:read")); - - policyEngine.registerPostValidator(RequestCatalogPolicyContext.class, contextMappingFunction::apply); - policyEngine.registerPostValidator(RequestContractNegotiationPolicyContext.class, contextMappingFunction::apply); - policyEngine.registerPostValidator(RequestTransferProcessPolicyContext.class, contextMappingFunction::apply); - policyEngine.registerPostValidator(RequestVersionPolicyContext.class, contextMappingFunction::apply); - - - //register scope extractor - scopeExtractorRegistry.registerScopeExtractor(new DataAccessCredentialScopeExtractor()); - - - typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(typeManager, JSON_LD)); - } -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DefaultScopeMappingFunction.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DefaultScopeMappingFunction.java deleted file mode 100644 index e580078f5..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DefaultScopeMappingFunction.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.core; - -import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; -import org.eclipse.edc.policy.engine.spi.PolicyValidatorRule; -import org.eclipse.edc.policy.model.Policy; - -import java.util.HashSet; -import java.util.Set; - -public class DefaultScopeMappingFunction implements PolicyValidatorRule { - private final Set defaultScopes; - - public DefaultScopeMappingFunction(Set defaultScopes) { - this.defaultScopes = defaultScopes; - } - - @Override - public Boolean apply(Policy policy, RequestPolicyContext requestPolicyContext) { - var requestScopeBuilder = requestPolicyContext.requestScopeBuilder(); - var rq = requestScopeBuilder.build(); - var existingScope = rq.getScopes(); - var newScopes = new HashSet<>(defaultScopes); - newScopes.addAll(existingScope); - requestScopeBuilder.scopes(newScopes); - return true; - } -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/AbstractCredentialEvaluationFunction.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/AbstractCredentialEvaluationFunction.java deleted file mode 100644 index 3d6992a48..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/AbstractCredentialEvaluationFunction.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; -import org.eclipse.edc.participant.spi.ParticipantAgent; -import org.eclipse.edc.spi.result.Result; - -import java.util.List; - -public class AbstractCredentialEvaluationFunction { - private static final String VC_CLAIM = "vc"; - protected static final String MVD_NAMESPACE = "https://w3id.org/mvd/credentials/"; - - protected Result> getCredentialList(ParticipantAgent agent) { - var vcListClaim = agent.getClaims().get(VC_CLAIM); - - if (vcListClaim == null) { - return Result.failure("ParticipantAgent did not contain a '%s' claim.".formatted(VC_CLAIM)); - } - if (!(vcListClaim instanceof List)) { - return Result.failure("ParticipantAgent contains a '%s' claim, but the type is incorrect. Expected %s, received %s.".formatted(VC_CLAIM, List.class.getName(), vcListClaim.getClass().getName())); - } - var vcList = (List) vcListClaim; - if (vcList.isEmpty()) { - return Result.failure("ParticipantAgent contains a '%s' claim but it did not contain any VerifiableCredentials.".formatted(VC_CLAIM)); - } - return Result.success(vcList); - } -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/DataAccessLevelFunction.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/DataAccessLevelFunction.java deleted file mode 100644 index 49ac49b3e..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/DataAccessLevelFunction.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -import org.eclipse.edc.participant.spi.ParticipantAgentPolicyContext; -import org.eclipse.edc.policy.engine.spi.AtomicConstraintRuleFunction; -import org.eclipse.edc.policy.model.Duty; -import org.eclipse.edc.policy.model.Operator; - -import java.util.Objects; - -public class DataAccessLevelFunction extends AbstractCredentialEvaluationFunction implements AtomicConstraintRuleFunction { - - private static final String DATAPROCESSOR_CRED_TYPE = "DataProcessorCredential"; - - private DataAccessLevelFunction() { - - } - - public static DataAccessLevelFunction create() { - return new DataAccessLevelFunction<>() { - }; - } - - @Override - public boolean evaluate(Operator operator, Object rightOperand, Duty duty, C policyContext) { - if (!operator.equals(Operator.EQ)) { - policyContext.reportProblem("Cannot evaluate operator %s, only %s is supported".formatted(operator, Operator.EQ)); - return false; - } - var pa = policyContext.participantAgent(); - if (pa == null) { - policyContext.reportProblem("ParticipantAgent not found on PolicyContext"); - return false; - } - - var credentialResult = getCredentialList(pa); - if (credentialResult.failed()) { - policyContext.reportProblem(credentialResult.getFailureDetail()); - return false; - } - - return credentialResult.getContent() - .stream() - .filter(vc -> vc.getType().stream().anyMatch(t -> t.endsWith(DATAPROCESSOR_CRED_TYPE))) - .flatMap(credential -> credential.getCredentialSubject().stream()) - .anyMatch(credentialSubject -> { - var version = credentialSubject.getClaim(MVD_NAMESPACE, "contractVersion"); - var level = credentialSubject.getClaim(MVD_NAMESPACE, "level"); - - return version != null && Objects.equals(level, rightOperand); - }); - - - } - -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java deleted file mode 100644 index 4f9e51525..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -import org.eclipse.edc.participant.spi.ParticipantAgentPolicyContext; -import org.eclipse.edc.policy.engine.spi.AtomicConstraintRuleFunction; -import org.eclipse.edc.policy.model.Operator; -import org.eclipse.edc.policy.model.Permission; - -import java.time.Instant; -import java.util.Map; - -public class MembershipCredentialEvaluationFunction extends AbstractCredentialEvaluationFunction implements AtomicConstraintRuleFunction { - public static final String MEMBERSHIP_CONSTRAINT_KEY = "MembershipCredential"; - - private static final String MEMBERSHIP_CLAIM = "membership"; - private static final String SINCE_CLAIM = "since"; - private static final String ACTIVE = "active"; - - private MembershipCredentialEvaluationFunction() { - } - - public static MembershipCredentialEvaluationFunction create() { - return new MembershipCredentialEvaluationFunction<>() { - }; - } - - @SuppressWarnings("unchecked") - @Override - public boolean evaluate(Operator operator, Object rightOperand, Permission permission, C policyContext) { - if (!operator.equals(Operator.EQ)) { - policyContext.reportProblem("Invalid operator '%s', only accepts '%s'".formatted(operator, Operator.EQ)); - return false; - } - if (!ACTIVE.equals(rightOperand)) { - policyContext.reportProblem("Right-operand must be equal to '%s', but was '%s'".formatted(ACTIVE, rightOperand)); - return false; - } - - var pa = policyContext.participantAgent(); - if (pa == null) { - policyContext.reportProblem("No ParticipantAgent found on context."); - return false; - } - var credentialResult = getCredentialList(pa); - if (credentialResult.failed()) { - policyContext.reportProblem(credentialResult.getFailureDetail()); - return false; - } - - return credentialResult.getContent() - .stream() - .filter(vc -> vc.getType().stream().anyMatch(t -> t.endsWith(MEMBERSHIP_CONSTRAINT_KEY))) - .flatMap(vc -> vc.getCredentialSubject().stream().filter(cs -> cs.getClaims().containsKey(MEMBERSHIP_CLAIM))) - .anyMatch(credential -> { - var membershipClaim = (Map) credential.getClaim(MVD_NAMESPACE, MEMBERSHIP_CLAIM); - var membershipStartDate = Instant.parse(membershipClaim.get(SINCE_CLAIM).toString()); - return membershipStartDate.isBefore(Instant.now()); - }); - } - -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java deleted file mode 100644 index 01b761a17..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyEvaluationExtension.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -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.engine.spi.AtomicConstraintRuleFunction; -import org.eclipse.edc.policy.engine.spi.PolicyContext; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; -import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; -import org.eclipse.edc.policy.model.Duty; -import org.eclipse.edc.policy.model.Permission; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; - -import static org.eclipse.edc.demo.dcp.policy.MembershipCredentialEvaluationFunction.MEMBERSHIP_CONSTRAINT_KEY; -import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; - -public class PolicyEvaluationExtension implements ServiceExtension { - - @Inject - private PolicyEngine policyEngine; - - @Inject - private RuleBindingRegistry ruleBindingRegistry; - - @Override - public void initialize(ServiceExtensionContext context) { - - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), TransferProcessPolicyContext.class, TransferProcessPolicyContext.TRANSFER_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), ContractNegotiationPolicyContext.class, ContractNegotiationPolicyContext.NEGOTIATION_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - bindPermissionFunction(MembershipCredentialEvaluationFunction.create(), CatalogPolicyContext.class, CatalogPolicyContext.CATALOG_SCOPE, MEMBERSHIP_CONSTRAINT_KEY); - - registerDataAccessLevelFunction(); - - } - - private void registerDataAccessLevelFunction() { - var accessLevelKey = "DataAccess.level"; - - bindDutyFunction(DataAccessLevelFunction.create(), TransferProcessPolicyContext.class, TransferProcessPolicyContext.TRANSFER_SCOPE, accessLevelKey); - bindDutyFunction(DataAccessLevelFunction.create(), ContractNegotiationPolicyContext.class, ContractNegotiationPolicyContext.NEGOTIATION_SCOPE, accessLevelKey); - bindDutyFunction(DataAccessLevelFunction.create(), CatalogPolicyContext.class, CatalogPolicyContext.CATALOG_SCOPE, accessLevelKey); - } - - private void bindPermissionFunction(AtomicConstraintRuleFunction function, Class contextClass, String scope, String constraintType) { - ruleBindingRegistry.bind("use", scope); - ruleBindingRegistry.bind(ODRL_SCHEMA + "use", scope); - ruleBindingRegistry.bind(constraintType, scope); - - policyEngine.registerFunction(contextClass, Permission.class, constraintType, function); - } - - private void bindDutyFunction(AtomicConstraintRuleFunction function, Class contextClass, String scope, String constraintType) { - ruleBindingRegistry.bind("use", scope); - ruleBindingRegistry.bind(ODRL_SCHEMA + "use", scope); - ruleBindingRegistry.bind(constraintType, scope); - - policyEngine.registerFunction(contextClass, Duty.class, constraintType, function); - } -} diff --git a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyScopes.java b/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyScopes.java deleted file mode 100644 index 1ce4d171f..000000000 --- a/extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/PolicyScopes.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.policy; - -/** - * Defines standard EDC policy scopes. - */ -public interface PolicyScopes { - String CATALOG_REQUEST_SCOPE = "request.catalog"; - String NEGOTIATION_REQUEST_SCOPE = "request.contract.negotiation"; - String TRANSFER_PROCESS_REQUEST_SCOPE = "request.transfer.process"; - - String CATALOG_SCOPE = "catalog"; - String NEGOTIATION_SCOPE = "contract.negotiation"; - String TRANSFER_PROCESS_SCOPE = "transfer.process"; -} diff --git a/extensions/dcp-impl/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/dcp-impl/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index b26ac18f9..000000000 --- a/extensions/dcp-impl/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (c) 2023 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 -# -# - -org.eclipse.edc.demo.dcp.core.DcpPatchExtension -org.eclipse.edc.demo.dcp.policy.PolicyEvaluationExtension diff --git a/extensions/did-example-resolver/build.gradle.kts b/extensions/did-example-resolver/build.gradle.kts deleted file mode 100644 index 8b49fe622..000000000 --- a/extensions/did-example-resolver/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -/* -* 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-library` -} - -dependencies { - implementation(libs.edc.did.core) - implementation(libs.edc.ih.spi.did) -} diff --git a/extensions/did-example-resolver/src/main/java/org/eclipse/edc/iam/identitytrust/core/SecretsExtension.java b/extensions/did-example-resolver/src/main/java/org/eclipse/edc/iam/identitytrust/core/SecretsExtension.java deleted file mode 100644 index d55d3c1e9..000000000 --- a/extensions/did-example-resolver/src/main/java/org/eclipse/edc/iam/identitytrust/core/SecretsExtension.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.iam.identitytrust.core; - -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.security.Vault; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; - - -public class SecretsExtension implements ServiceExtension { - // duplicated from DcpDefaultServicesExtension - private static final String STS_PRIVATE_KEY_ALIAS = "edc.iam.sts.privatekey.alias"; - private static final String STS_PUBLIC_KEY_ID = "edc.iam.sts.publickey.id"; - @Inject - private Vault vault; - - @Override - public void initialize(ServiceExtensionContext context) { - seedKeys(context); - } - - /** - * We need this, because we don't have a vault that is shared between Connector and IdentityHub, so this needs to be seeded to either of them. - * - * @param context the service extension context used for accessing configuration and other services - */ - private void seedKeys(ServiceExtensionContext context) { - // Let's avoid pulling in the connector-core module, just for the instanceof check - if (vault.getClass().getSimpleName().equals("InMemoryVault")) { - var publicKey = """ - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5r - oYnkAXuqCYfNK3ex+hMWFuiXGUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== - -----END PUBLIC KEY----- - """; - - var privateKey = """ - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIARDUGJgKy1yzxkueIJ1k3MPUWQ/tbQWQNqW6TjyHpdcoAoGCCqGSM49 - AwEHoUQDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5roYnkAXuqCYfNK3ex+hMWFuiX - GUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== - -----END EC PRIVATE KEY----- - """; - - - vault.storeSecret(context.getConfig().getString(STS_PRIVATE_KEY_ALIAS), privateKey); - vault.storeSecret(context.getConfig().getString(STS_PUBLIC_KEY_ID), publicKey); - - context.getMonitor().withPrefix("DEMO").warning(">>>>>> This extension hard-codes a keypair into the vault! <<<<<<"); - } - } -} diff --git a/extensions/did-example-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/did-example-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index e8aee0b40..000000000 --- a/extensions/did-example-resolver/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright (c) 2024 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 -# -# - -org.eclipse.edc.iam.identitytrust.core.SecretsExtension \ No newline at end of file diff --git a/extensions/superuser-seed/build.gradle.kts b/extensions/superuser-seed/build.gradle.kts deleted file mode 100644 index 461762a40..000000000 --- a/extensions/superuser-seed/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -/* -* 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-library` -} - -dependencies { - implementation(libs.edc.ih.spi.credentials) - implementation(libs.edc.ih.spi) - testImplementation(libs.edc.junit) - -} diff --git a/extensions/superuser-seed/src/main/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtension.java b/extensions/superuser-seed/src/main/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtension.java deleted file mode 100644 index df2634a0e..000000000 --- a/extensions/superuser-seed/src/main/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtension.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024 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.identityhub.seed; - -import org.eclipse.edc.identityhub.spi.authentication.ServicePrincipal; -import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; -import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; -import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.security.Vault; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; - -import java.util.List; -import java.util.Map; - -import static java.util.Optional.ofNullable; - -public class ParticipantContextSeedExtension implements ServiceExtension { - public static final String NAME = "MVD ParticipantContext Seed Extension"; - public static final String DEFAULT_SUPER_USER_PARTICIPANT_ID = "super-user"; - - @Setting(value = "Explicitly set the initial API key for the Super-User") - public static final String SUPERUSER_APIKEY_PROPERTY = "edc.ih.api.superuser.key"; - - @Setting(value = "Config value to set the super-user's participant ID.", defaultValue = DEFAULT_SUPER_USER_PARTICIPANT_ID) - public static final String SUPERUSER_PARTICIPANT_ID_PROPERTY = "edc.ih.api.superuser.id"; - private String superUserParticipantId; - private String superUserApiKey; - private Monitor monitor; - @Inject - private ParticipantContextService participantContextService; - @Inject - private Vault vault; - - @Override - public String name() { - return NAME; - } - - @Override - public void initialize(ServiceExtensionContext context) { - superUserParticipantId = context.getSetting(SUPERUSER_PARTICIPANT_ID_PROPERTY, DEFAULT_SUPER_USER_PARTICIPANT_ID); - superUserApiKey = context.getSetting(SUPERUSER_APIKEY_PROPERTY, null); - monitor = context.getMonitor(); - } - - @Override - public void start() { - // create super-user - if (participantContextService.getParticipantContext(superUserParticipantId).succeeded()) { // already exists - monitor.debug("super-user already exists with ID '%s', will not re-create".formatted(superUserParticipantId)); - return; - } - participantContextService.createParticipantContext(ParticipantManifest.Builder.newInstance() - .participantId(superUserParticipantId) - .did("did:web:%s".formatted(superUserParticipantId)) // doesn't matter, not intended for resolution - .active(true) - .key(KeyDescriptor.Builder.newInstance() - .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) - .keyId("%s-key".formatted(superUserParticipantId)) - .privateKeyAlias("%s-alias".formatted(superUserParticipantId)) - .build()) - .roles(List.of(ServicePrincipal.ROLE_ADMIN)) - .build()) - .onSuccess(generatedKey -> { - var apiKey = ofNullable(superUserApiKey) - .map(key -> { - if (!key.contains(".")) { - monitor.warning("Super-user key override: this key appears to have an invalid format, you may be unable to access some APIs. It must follow the structure: 'base64().'"); - } - participantContextService.getParticipantContext(superUserParticipantId) - .onSuccess(pc -> vault.storeSecret(pc.getApiTokenAlias(), key) - .onSuccess(u -> monitor.debug("Super-user key override successful")) - .onFailure(f -> monitor.warning("Error storing API key in vault: %s".formatted(f.getFailureDetail())))) - .onFailure(f -> monitor.warning("Error overriding API key for '%s': %s".formatted(superUserParticipantId, f.getFailureDetail()))); - return key; - }) - .orElse(generatedKey.apiKey()); - monitor.info("Created user 'super-user'. Please take note of the API Key: %s".formatted(apiKey)); - }) - .orElseThrow(f -> new EdcException("Error creating Super-User: " + f.getFailureDetail())); - } -} diff --git a/extensions/superuser-seed/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/superuser-seed/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index 94348f1c8..000000000 --- a/extensions/superuser-seed/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright (c) 2024 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 -# -# - -org.eclipse.edc.identityhub.seed.ParticipantContextSeedExtension \ No newline at end of file diff --git a/extensions/superuser-seed/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java b/extensions/superuser-seed/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java deleted file mode 100644 index 3b12ab8fb..000000000 --- a/extensions/superuser-seed/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2024 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.identityhub.seed; - -import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; -import org.eclipse.edc.identityhub.spi.participantcontext.model.CreateParticipantContextResponse; -import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; -import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.result.Result; -import org.eclipse.edc.spi.result.ServiceResult; -import org.eclipse.edc.spi.security.Vault; -import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -@ExtendWith(DependencyInjectionExtension.class) -class ParticipantContextSeedExtensionTest { - - public static final String SUPER_USER = "super-user"; - public static final String API_KEY = "apiKey"; - private final ParticipantContextService participantContextService = mock(); - private final Vault vault = mock(); - private final Monitor monitor = mock(); - - @BeforeEach - void setup(ServiceExtensionContext context) { - context.registerService(ParticipantContextService.class, participantContextService); - context.registerService(Vault.class, vault); - context.registerService(Monitor.class, monitor); - when(participantContextService.getParticipantContext(eq(SUPER_USER))).thenReturn(ServiceResult.notFound("foobar")); - } - - @Test - void start_verifySuperUser(ParticipantContextSeedExtension ext, - ServiceExtensionContext context) { - - when(participantContextService.createParticipantContext(any())).thenReturn(ServiceResult.success(new CreateParticipantContextResponse("some-key", null, null))); - ext.initialize(context); - - ext.start(); - verify(participantContextService).getParticipantContext(eq(SUPER_USER)); - verify(participantContextService).createParticipantContext(any()); - verifyNoMoreInteractions(participantContextService); - } - - @Test - void start_failsToCreate(ParticipantContextSeedExtension ext, ServiceExtensionContext context) { - - when(participantContextService.createParticipantContext(any())) - .thenReturn(ServiceResult.badRequest("test-message")); - ext.initialize(context); - assertThatThrownBy(ext::start).isInstanceOf(EdcException.class); - - verify(participantContextService).getParticipantContext(eq(SUPER_USER)); - verify(participantContextService).createParticipantContext(any()); - verifyNoMoreInteractions(participantContextService); - } - - @Test - void start_withApiKeyOverride(ParticipantContextSeedExtension ext, - ServiceExtensionContext context) { - - - when(vault.storeSecret(any(), any())).thenReturn(Result.success()); - - var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; - when(context.getSetting(eq(ParticipantContextSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) - .thenReturn(apiKeyOverride); - - when(participantContextService.createParticipantContext(any())) - .thenReturn(ServiceResult.success(new CreateParticipantContextResponse("generated-api-key", null, null))); - when(participantContextService.getParticipantContext(eq(SUPER_USER))) - .thenReturn(ServiceResult.notFound("foobar")) - .thenReturn(ServiceResult.success(superUserContext().build())); - - ext.initialize(context); - ext.start(); - verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); - verify(participantContextService).createParticipantContext(any()); - verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); - verifyNoMoreInteractions(participantContextService, vault); - } - - @Test - void start_withInvalidKeyOverride(ParticipantContextSeedExtension ext, - ServiceExtensionContext context) { - when(vault.storeSecret(any(), any())).thenReturn(Result.success()); - - var apiKeyOverride = "some-invalid-key"; - when(context.getSetting(eq(ParticipantContextSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) - .thenReturn(apiKeyOverride); - - when(participantContextService.createParticipantContext(any())) - .thenReturn(ServiceResult.success(new CreateParticipantContextResponse("generated-api-key", null, null))); - when(participantContextService.getParticipantContext(eq(SUPER_USER))) - .thenReturn(ServiceResult.notFound("foobar")) - .thenReturn(ServiceResult.success(superUserContext().build())); - - ext.initialize(context); - ext.start(); - verify(participantContextService).createParticipantContext(any()); - verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); - verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); - verify(monitor).warning(contains("this key appears to have an invalid format")); - verifyNoMoreInteractions(participantContextService, vault); - } - - @Test - void start_whenVaultReturnsFailure(ParticipantContextSeedExtension ext, - ServiceExtensionContext context) { - when(vault.storeSecret(any(), any())).thenReturn(Result.failure("test-failure")); - - var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; - when(context.getSetting(eq(ParticipantContextSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) - .thenReturn(apiKeyOverride); - - when(participantContextService.createParticipantContext(any())) - .thenReturn(ServiceResult.success(new CreateParticipantContextResponse("generated-api-key", null, null))); - when(participantContextService.getParticipantContext(eq(SUPER_USER))) - .thenReturn(ServiceResult.notFound("foobar")) - .thenReturn(ServiceResult.success(superUserContext().build())); - - ext.initialize(context); - ext.start(); - verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); - verify(participantContextService).createParticipantContext(any()); - verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); - verify(monitor).warning(eq("Error storing API key in vault: test-failure")); - verifyNoMoreInteractions(participantContextService, vault); - } - - private ParticipantContext.Builder superUserContext() { - return ParticipantContext.Builder.newInstance() - .participantContextId(SUPER_USER) - .apiTokenAlias("super-user-apikey"); - - } -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e6e4c6a03..0b55ba92d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=org.eclipse.edc -version=0.15.0-SNAPSHOT +version=0.17.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97eb57c36..9a49a7999 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,13 +3,15 @@ format.version = "1.1" [versions] awaitility = "4.3.0" -edc = "0.14.1" +edc = "0.17.0-SNAPSHOT" edc-build = "1.3.0" jackson = "2.21.2" jakarta-json = "2.1.3" parsson = "1.1.6" postgres = "42.7.10" restAssured = "6.0.0" +rsApi = "4.0.0" +swagger = "2.2.44" [libraries] # upstream EDC dependencies @@ -17,7 +19,9 @@ edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } edc-config-fs = { module = "org.eclipse.edc:configuration-filesystem", version.ref = "edc" } edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } edc-did-core = { module = "org.eclipse.edc:identity-did-core", version.ref = "edc" } +edc-core-jersey = { module = "org.eclipse.edc:jersey-core", version.ref = "edc" } edc-did-web = { module = "org.eclipse.edc:identity-did-web", version.ref = "edc" } +edc-core-participantcontext-config = { module = "org.eclipse.edc:participant-context-config-core", version.ref = "edc" } edc-core-api = { module = "org.eclipse.edc:api-core", version.ref = "edc" } edc-core-connector = { module = "org.eclipse.edc:connector-core", version.ref = "edc" } edc-core-token = { module = "org.eclipse.edc:token-core", version.ref = "edc" } @@ -26,7 +30,7 @@ edc-core-edrstore = { module = "org.eclipse.edc:edr-store-core", version.ref = " edc-ext-http = { module = "org.eclipse.edc:http", version.ref = "edc" } edc-ext-jsonld = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } edc-api-dsp-config = { module = "org.eclipse.edc:dsp-http-api-configuration-2025", version.ref = "edc" } -edc-dcp = { module = "org.eclipse.edc:identity-trust-service", version.ref = "edc" } +edc-dcp = { module = "org.eclipse.edc:decentralized-claims-service", version.ref = "edc" } edc-controlplane-core = { module = "org.eclipse.edc:control-plane-core", version.ref = "edc" } edc-controlplane-transform = { module = "org.eclipse.edc:control-plane-transform", version.ref = "edc" } edc-controlplane-services = { module = "org.eclipse.edc:control-plane-aggregate-services", version.ref = "edc" } @@ -37,12 +41,15 @@ edc-api-observability = { module = "org.eclipse.edc:api-observability", version. edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dataplane-v2 = { module = "org.eclipse.edc:data-plane-public-api-v2", version.ref = "edc" } -edc-dcp-core = { module = "org.eclipse.edc:identity-trust-core", version.ref = "edc" } +edc-dcp-core = { module = "org.eclipse.edc:decentralized-claims-core", version.ref = "edc" } edc-vault-hashicorp = { module = "org.eclipse.edc:vault-hashicorp", version.ref = "edc" } -edc-spi-identity-trust = { module = "org.eclipse.edc:identity-trust-spi", version.ref = "edc" } +edc-spi-identity-trust = { module = "org.eclipse.edc:decentralized-claims-spi", version.ref = "edc" } edc-spi-transform = { module = "org.eclipse.edc:transform-spi", version.ref = "edc" } edc-spi-catalog = { module = "org.eclipse.edc:catalog-spi", version.ref = "edc" } edc-spi-identity-did = { module = "org.eclipse.edc:identity-did-spi", version.ref = "edc" } +edc-spi-http = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } +edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } +edc-spi-dataplane = { module = "org.eclipse.edc:data-plane-spi", version.ref = "edc" } # EDC lib dependencies edc-lib-jws2020 = { module = "org.eclipse.edc:jws2020-lib", version.ref = "edc" } @@ -50,6 +57,10 @@ edc-lib-transform = { module = "org.eclipse.edc:transform-lib", version.ref = "e edc-lib-crypto = { module = "org.eclipse.edc:crypto-common-lib", version.ref = "edc" } edc-lib-keys = { module = "org.eclipse.edc:keys-lib", version.ref = "edc" } edc-lib-jsonld = { module = "org.eclipse.edc:json-ld-lib", version.ref = "edc" } +edc-lib-http = { module = "org.eclipse.edc:http-lib", version.ref = "edc" } +edc-lib-util = { module = "org.eclipse.edc:util-lib", version.ref = "edc" } +edc-lib-sql = { module = "org.eclipse.edc:sql-lib", version.ref = "edc" } +edc-lib-util-dataplane = { module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } # EDC Postgres modules edc-sql-assetindex = { module = "org.eclipse.edc:asset-index-sql", version.ref = "edc" } @@ -80,18 +91,13 @@ edc-issuance-spi = { module = "org.eclipse.edc:issuerservice-issuance-spi", vers # EDC STS dependencies, used in IdentityHub edc-sts-remote-client = { module = "org.eclipse.edc:identity-trust-sts-remote-client", version.ref = "edc" } -# federated catalog modules -edc-fc-spi-crawler = { module = "org.eclipse.edc:crawler-spi", version.ref = "edc" } -edc-fc-core = { module = "org.eclipse.edc:federated-catalog-core", version.ref = "edc" } -edc-fc-core2025 = { module = "org.eclipse.edc:federated-catalog-core-2025", version.ref = "edc" } -edc-fc-core08 = { module = "org.eclipse.edc:federated-catalog-core-08", version.ref = "edc" } -edc-fc-api = { module = "org.eclipse.edc:federated-catalog-api", version.ref = "edc" } - # Third party libs postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } jakarta-json-api = { module = "jakarta.json:jakarta.json-api", version.ref = "jakarta-json" } +jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "rsApi" } +tink = { module = "com.google.crypto.tink:tink", version = "1.20.0" } jackson-datatype-jakarta-jsonp = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jakarta-jsonp", version.ref = "jackson" } parsson = { module = "org.eclipse.parsson:parsson", version.ref = "parsson" } @@ -144,3 +150,4 @@ sql-edc = [ [plugins] shadow = { id = "com.gradleup.shadow", version = "9.4.0" } edc-build = { id = "org.eclipse.edc.edc-build", version.ref = "edc-build" } +swagger = { id = "io.swagger.core.v3.swagger-gradle-plugin", version.ref = "swagger" } diff --git a/k8s/consumer/application/controlplane-config.yaml b/k8s/consumer/application/controlplane-config.yaml new file mode 100644 index 000000000..9fd1e07a5 --- /dev/null +++ b/k8s/consumer/application/controlplane-config.yaml @@ -0,0 +1,68 @@ +# +# 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 +# + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: controlplane-config + namespace: consumer +data: + # base config + edc.hostname: "controlplane.consumer.svc.cluster.local" + edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + edc.participant.id: "did:web:connector" + edc.iam.did.web.use.https: "false" + edc.iam.credential.revocation.mimetype: "application/json" + + # web config + web.http.port: "8080" + web.http.path: "/api" + web.http.protocol.port: "8082" + web.http.protocol.path: "/api/dsp" + web.http.management.port: "8081" + web.http.management.path: "/api/mgmt" + web.http.control.port: "8083" + web.http.control.path: "/api/control" + + # dataplane config + edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/controlplane" + edc.datasource.default.user: "cp" + edc.datasource.default.password: "cp" + edc.sql.schema.autocreate: "true" + + edc.encryption.aes.key.alias: "aes-key-alias" + + # Oauth2 config + # KeyCloak takes the `iss` claim's host from the request URL. For now, this is the URL defined in the ingress route. + # to do this properly, we should probably configure the following properties on the ingress route: + # proxy_set_header Host $host; + # proxy_set_header X-Forwarded-Proto $scheme; + edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" + + # Default scopes config + edc.iam.dcp.scopes.membership.id: "membership-scope" + edc.iam.dcp.scopes.membership.type: "DEFAULT" + edc.iam.dcp.scopes.membership.value: "org.eclipse.edc.vc.type:MembershipCredential:read" + + edc.iam.dcp.scopes.manufacturer.id: "manufacturer-scope" + edc.iam.dcp.scopes.manufacturer.type: "POLICY" + edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.edc.vc.type:ManufacturerCredential:read" + edc.iam.dcp.scopes.manufacturer.prefix-mapping: "ManufacturerCredential" + + # Trusted Issuers + edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.consumer.svc.cluster.local%3A10016:issuer" + + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" \ No newline at end of file diff --git a/k8s/consumer/application/controlplane.yaml b/k8s/consumer/application/controlplane.yaml new file mode 100644 index 000000000..7be42a8d6 --- /dev/null +++ b/k8s/consumer/application/controlplane.yaml @@ -0,0 +1,112 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controlplane + namespace: consumer + labels: + app: controlplane + type: edc-app +spec: + replicas: 1 + selector: + matchLabels: + app: controlplane + template: + metadata: + name: controlplane + labels: + app: controlplane + platform: edcv + type: edc-app + spec: + containers: + - name: controlplane + image: ghcr.io/eclipse-edc/mvd/controlplane:latest + imagePullPolicy: Never + envFrom: + - configMapRef: { name: controlplane-config } + ports: + - containerPort: 1044 + name: debug-port + livenessProbe: + httpGet: + path: /api/check/liveness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + startupProbe: + httpGet: + path: /api/check/startup + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + restartPolicy: Always + +--- +apiVersion: v1 +kind: Service +metadata: + name: controlplane + namespace: consumer +spec: + type: ClusterIP + selector: + app: controlplane + ports: + - name: health + port: 8080 + targetPort: 8080 + - name: management + port: 8081 + targetPort: 8081 + - name: protocol + port: 8082 + targetPort: 8082 + - name: debug + port: 1044 + targetPort: 1044 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: controlplane + namespace: consumer +spec: + parentRefs: + - name: consumer-gateway + kind: Gateway + sectionName: http + hostnames: + - cp.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: controlplane + port: 8081 + weight: 1 \ No newline at end of file diff --git a/k8s/consumer/application/identityhub-config.yaml b/k8s/consumer/application/identityhub-config.yaml new file mode 100644 index 000000000..5af7449c3 --- /dev/null +++ b/k8s/consumer/application/identityhub-config.yaml @@ -0,0 +1,55 @@ +# +# 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 +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: ih-config + namespace: consumer +data: + edc.hostname: "identityhub.consumer.svc.cluster.local" + edc.iam.credentia.revocation.mimetype: "application/json" + edc.iam.did.web.use.https: "false" + edc.ih.iam.publickey.alias: "publickey-alias" + web.http.port: "7080" + web.http.path: "/api" + web.http.identity.port: "7081" + web.http.identity.path: "/api/identity" + web.http.identity.auth.key: "password" + web.http.credentials.port: "7082" + web.http.credentials.path: "/api/credentials" + web.http.did.port: "7083" + web.http.did.path: "/" + web.http.sts.port: "7084" + web.http.sts.path: "/api/sts" + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" + + edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + + edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/identityhub" + edc.datasource.default.user: "ih" + edc.datasource.default.password: "ih" + edc.sql.schema.autocreate: "true" + edc.iam.accesstoken.jti.validation: "true" + # grace period for credential expiry, 3600*24 = 1 day + edc.iam.credential.renewal.graceperiod: "86400" + + edc.encryption.aes.key.alias: "aes-key-alias" + # Oauth2 config + # KeyCloak takes the `iss` claim's host from the request URL. For now, this is the URL defined in the ingress route. + # to do this properly, we should probably configure the following properties on the ingress route: + # proxy_set_header Host $host; + # proxy_set_header X-Forwarded-Proto $scheme; + edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file diff --git a/k8s/consumer/application/identityhub.yaml b/k8s/consumer/application/identityhub.yaml new file mode 100644 index 000000000..3b290f946 --- /dev/null +++ b/k8s/consumer/application/identityhub.yaml @@ -0,0 +1,128 @@ +# +# Copyright (c) 2026 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 +# + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: identityhub + namespace: consumer + labels: + app: identityhub + type: edc-app +spec: + replicas: 1 + selector: + matchLabels: + app: identityhub + template: + metadata: + labels: + app: identityhub + type: edc-app + spec: + containers: + - name: identityhub + image: ghcr.io/eclipse-edc/mvd/identity-hub:latest + imagePullPolicy: Never + envFrom: + - configMapRef: + name: ih-config + ports: + - containerPort: 1044 + name: debug + livenessProbe: + httpGet: + path: /api/check/liveness + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + startupProbe: + httpGet: + path: /api/check/startup + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + +--- +apiVersion: v1 +kind: Service +metadata: + name: identityhub + namespace: consumer +spec: + type: ClusterIP + selector: + app: identityhub + ports: + - port: 7082 + targetPort: 7082 + name: creds-port + - port: 1044 + targetPort: 1044 + name: debug + - port: 7081 + targetPort: 7081 + name: identity-api + - port: 7083 + targetPort: 7083 + name: did + - port: 7084 + targetPort: 7084 + name: sts + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: identityhub + namespace: consumer +spec: + parentRefs: + - name: edcv-gateway + kind: Gateway + sectionName: http + hostnames: + - ih.localhost + rules: + - matches: + - path: + type: PathPrefix + value: /cs + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: identityhub + port: 7081 + + # DID Route + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: identityhub + port: 7083 \ No newline at end of file diff --git a/k8s/consumer/base/gateway-class.yaml b/k8s/consumer/base/gateway-class.yaml new file mode 100644 index 000000000..2a209380e --- /dev/null +++ b/k8s/consumer/base/gateway-class.yaml @@ -0,0 +1,20 @@ +# +# 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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: traefik +spec: + controllerName: traefik.io/gateway-controller \ No newline at end of file diff --git a/k8s/consumer/base/gateway.yaml b/k8s/consumer/base/gateway.yaml new file mode 100644 index 000000000..ebcce40ad --- /dev/null +++ b/k8s/consumer/base/gateway.yaml @@ -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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: consumer-gateway + namespace: consumer +spec: + gatewayClassName: traefik + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All #or Same or Selector +# kinds: +# - kind: HTTPRoute +# group: gateway.networking.k8s.io \ No newline at end of file diff --git a/k8s/consumer/base/namespace.yaml b/k8s/consumer/base/namespace.yaml new file mode 100644 index 000000000..ed98e925d --- /dev/null +++ b/k8s/consumer/base/namespace.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: v1 +kind: Namespace +metadata: + name: consumer diff --git a/k8s/consumer/base/postgres.yaml b/k8s/consumer/base/postgres.yaml new file mode 100644 index 000000000..5f4c3f659 --- /dev/null +++ b/k8s/consumer/base/postgres.yaml @@ -0,0 +1,83 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: consumer + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + platform: edcv + type: edc-infra + spec: + containers: + - name: postgres + image: postgres:17.7-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "controlplane" + - name: POSTGRES_USER + value: "cp" + - name: POSTGRES_PASSWORD + value: "cp" + volumeMounts: + - name: init-script + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init-script + configMap: + name: postgres-init +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: consumer +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-init + namespace: consumer +data: + init.sql: | + CREATE DATABASE identityhub; + CREATE USER ih WITH PASSWORD 'ih'; + GRANT ALL PRIVILEGES ON DATABASE identityhub TO ih; + \c identityhub + GRANT ALL ON SCHEMA public TO ih; + + CREATE DATABASE dataplane; + CREATE USER dp WITH PASSWORD 'dp'; + GRANT ALL PRIVILEGES ON DATABASE dataplane TO dp; + \c dataplane + GRANT ALL ON SCHEMA public TO dp; \ No newline at end of file diff --git a/k8s/consumer/base/vault.yaml b/k8s/consumer/base/vault.yaml new file mode 100644 index 000000000..3e3579fa6 --- /dev/null +++ b/k8s/consumer/base/vault.yaml @@ -0,0 +1,80 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vault + namespace: consumer + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: vault + template: + metadata: + labels: + app: vault + type: edc-infra + spec: + containers: + - name: vault + image: hashicorp/vault:latest + ports: + - containerPort: 8200 + env: + - name: VAULT_DEV_ROOT_TOKEN_ID + value: "root" + - name: VAULT_DEV_LISTEN_ADDRESS + value: "0.0.0.0:8200" + args: + - "server" + - "-dev" + securityContext: + capabilities: + add: + - IPC_LOCK +--- +apiVersion: v1 +kind: Service +metadata: + name: vault + namespace: consumer +spec: + selector: + app: vault + ports: + - port: 8200 + targetPort: 8200 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: vault + namespace: consumer +spec: + parentRefs: + - name: consumer-gateway + hostnames: + - vault.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: vault + port: 8200 \ No newline at end of file diff --git a/k8s/consumer/kustomization.yml b/k8s/consumer/kustomization.yml new file mode 100644 index 000000000..59a1599c5 --- /dev/null +++ b/k8s/consumer/kustomization.yml @@ -0,0 +1,23 @@ +# +# Copyright (c) 2026 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 +# + +resources: + - base/namespace.yaml + - base/vault.yaml + - base/gateway.yaml + - base/gateway-class.yaml + - base/postgres.yaml + - application/controlplane-config.yaml + - application/controlplane.yaml + - application/identityhub-config.yaml + - application/identityhub.yaml \ No newline at end of file diff --git a/launchers/catalog-server/build.gradle.kts b/launchers/catalog-server/build.gradle.kts deleted file mode 100644 index b4bf9cdae..000000000 --- a/launchers/catalog-server/build.gradle.kts +++ /dev/null @@ -1,57 +0,0 @@ -/* -* 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-library` - id("application") - alias(libs.plugins.shadow) -} - -dependencies { - implementation(project(":extensions:did-example-resolver")) - implementation(project(":extensions:dcp-impl")) - runtimeOnly(libs.edc.api.secrets) - - runtimeOnly(libs.bundles.connector) // base runtime - runtimeOnly(libs.edc.api.management) - runtimeOnly(libs.edc.api.management.config) - runtimeOnly(libs.edc.controlplane.core) //default store impls, etc. - runtimeOnly(libs.edc.controlplane.services) // aggregate services - runtimeOnly(libs.edc.core.edrstore) - runtimeOnly(libs.edc.dsp) // protocol webhook - runtimeOnly(libs.bundles.dcp) // DCP protocol impl - runtimeOnly(libs.edc.api.dsp.config) // json-ld expansion - - if (project.properties.getOrDefault("persistence", "false") == "true") { - runtimeOnly(libs.edc.vault.hashicorp) - runtimeOnly(libs.bundles.sql.edc) - runtimeOnly(libs.edc.sts.remote.client) - println("This runtime compiles with a remote STS client, Hashicorp Vault and PostgreSQL. You will need properly configured Postgres and HCV instances.") - } - -} - -application { - mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") -} - -tasks.shadowJar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - mergeServiceFiles() - archiveFileName.set("catalog-server.jar") -} - -edcBuild { - publish.set(false) -} diff --git a/launchers/catalog-server/src/main/docker/Dockerfile b/launchers/catalog-server/src/main/docker/Dockerfile deleted file mode 100644 index d95740b56..000000000 --- a/launchers/catalog-server/src/main/docker/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# -buster is required to have apt available -FROM eclipse-temurin:23.0.2_7-jre-alpine - -# Optional JVM arguments, such as memory settings -ARG JVM_ARGS="" -ARG JAR - -RUN apk --no-cache add curl - -WORKDIR /app - - -COPY ${JAR} catalog-server.jar - -EXPOSE 8188 - -ENV WEB_HTTP_PORT="8080" -ENV WEB_HTTP_PATH="/api" - -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8080/api/check/health - -# Use "exec" for graceful termination (SIGINT) to reach JVM. -# ARG can not be used in ENTRYPOINT so storing value in an ENV variable -ENV ENV_JVM_ARGS=$JVM_ARGS -ENV ENV_APPINSIGHTS_AGENT_VERSION=$APPINSIGHTS_AGENT_VERSION -ENTRYPOINT [ "sh", "-c", "exec java $ENV_JVM_ARGS -jar catalog-server.jar --log-level=debug"] \ No newline at end of file diff --git a/launchers/controlplane/build.gradle.kts b/launchers/controlplane/build.gradle.kts index d09d3d2a0..202648163 100644 --- a/launchers/controlplane/build.gradle.kts +++ b/launchers/controlplane/build.gradle.kts @@ -19,17 +19,10 @@ plugins { } dependencies { - runtimeOnly(project(":extensions:did-example-resolver")) - runtimeOnly(project(":extensions:dcp-impl")) // some patches/impls for DCP - runtimeOnly(project(":extensions:catalog-node-resolver")) // to trigger the federated catalog runtimeOnly(libs.edc.bom.controlplane) runtimeOnly(libs.edc.api.secrets) - - if (project.properties.getOrDefault("persistence", "false") == "true") { - runtimeOnly(libs.edc.vault.hashicorp) - runtimeOnly(libs.edc.bom.controlplane.sql) - println("This runtime compiles with a remote STS client, Hashicorp Vault and PostgreSQL. You will need properly configured Postgres and HCV instances.") - } + runtimeOnly(libs.edc.vault.hashicorp) + runtimeOnly(libs.edc.bom.controlplane.sql) } tasks.shadowJar { diff --git a/launchers/controlplane/src/main/docker/Dockerfile b/launchers/controlplane/src/main/docker/Dockerfile index 39522c504..702acf531 100644 --- a/launchers/controlplane/src/main/docker/Dockerfile +++ b/launchers/controlplane/src/main/docker/Dockerfile @@ -14,9 +14,6 @@ COPY ${JAR} edc-controlplane.jar EXPOSE 8080 -ENV WEB_HTTP_PORT="8080" -ENV WEB_HTTP_PATH="/" - HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8080/api/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. diff --git a/launchers/dataplane/build.gradle.kts b/launchers/dataplane/build.gradle.kts index 71964e2a6..3bd0b7f8e 100644 --- a/launchers/dataplane/build.gradle.kts +++ b/launchers/dataplane/build.gradle.kts @@ -19,14 +19,15 @@ plugins { } dependencies { - runtimeOnly(libs.edc.bom.dataplane) - runtimeOnly(libs.edc.dataplane.v2) - - if (project.properties.getOrDefault("persistence", "false") == "true") { - runtimeOnly(libs.edc.vault.hashicorp) - runtimeOnly(libs.edc.bom.dataplane.sql) - println("This runtime compiles with a remote STS client, Hashicorp Vault and PostgreSQL. You will need properly configured Postgres and HCV instances.") + runtimeOnly(libs.tink) + implementation(libs.edc.bom.dataplane) { + exclude("org.eclipse.edc", "data-plane-self-registration") } + runtimeOnly(project(":extensions:data-plane-public-api-v2")) + + runtimeOnly(libs.edc.core.participantcontext.config) + runtimeOnly(libs.edc.vault.hashicorp) + runtimeOnly(libs.edc.bom.dataplane.sql) } tasks.shadowJar { diff --git a/launchers/dataplane/src/main/docker/Dockerfile b/launchers/dataplane/src/main/docker/Dockerfile index ffdd299b9..eed90712f 100644 --- a/launchers/dataplane/src/main/docker/Dockerfile +++ b/launchers/dataplane/src/main/docker/Dockerfile @@ -14,9 +14,6 @@ COPY ${JAR} edc-dataplane.jar EXPOSE 8080 -ENV WEB_HTTP_PORT="8080" -ENV WEB_HTTP_PATH="/" - HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8080/api/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. diff --git a/launchers/identity-hub/build.gradle.kts b/launchers/identity-hub/build.gradle.kts index a1d0ea1da..f0ae0bb9c 100644 --- a/launchers/identity-hub/build.gradle.kts +++ b/launchers/identity-hub/build.gradle.kts @@ -19,18 +19,9 @@ plugins { } dependencies { - runtimeOnly(project(":extensions:superuser-seed")) - runtimeOnly(project(":extensions:did-example-resolver")) - - implementation(libs.edc.ih.spi) // needed in the extensions here - implementation(libs.edc.ih.spi.credentials) // needed in the extensions here - runtimeOnly(libs.edc.bom.identityhub) - if (project.properties.getOrDefault("persistence", "false") == "true") { - runtimeOnly(libs.edc.vault.hashicorp) - runtimeOnly(libs.edc.bom.identityhub.sql) - println("This runtime compiles with a remote STS, Hashicorp Vault and PostgreSQL. You will need properly configured STS, Postgres and HCV instances.") - } + runtimeOnly(libs.edc.vault.hashicorp) + runtimeOnly(libs.edc.bom.identityhub.sql) testImplementation(libs.edc.spi.identity.did) testImplementation(libs.edc.lib.crypto) diff --git a/launchers/identity-hub/src/main/docker/Dockerfile b/launchers/identity-hub/src/main/docker/Dockerfile index 89d704029..d9dbad10f 100644 --- a/launchers/identity-hub/src/main/docker/Dockerfile +++ b/launchers/identity-hub/src/main/docker/Dockerfile @@ -14,9 +14,6 @@ COPY ${JAR} identity-hub.jar EXPOSE 8188 -ENV WEB_HTTP_PORT="8080" -ENV WEB_HTTP_PATH="/api" - HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8080/api/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. diff --git a/launchers/identity-hub/src/main/java/org/eclipse/edc/demo/dcp/ih/IdentityHubExtension.java b/launchers/identity-hub/src/main/java/org/eclipse/edc/demo/dcp/ih/IdentityHubExtension.java deleted file mode 100644 index 005d8a303..000000000 --- a/launchers/identity-hub/src/main/java/org/eclipse/edc/demo/dcp/ih/IdentityHubExtension.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 - * - */ - -package org.eclipse.edc.demo.dcp.ih; - -import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; -import org.eclipse.edc.identityhub.spi.verifiablecredentials.store.CredentialStore; -import org.eclipse.edc.runtime.metamodel.annotation.Extension; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.types.TypeManager; - -import java.io.File; -import java.io.IOException; -import java.util.stream.Stream; - -import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; - - -@Extension("DCP Demo: Core Extension for IdentityHub") -public class IdentityHubExtension implements ServiceExtension { - - @Inject - private CredentialStore store; - - @Inject - private TypeManager typeManager; - private String credentialsDir; - private Monitor monitor; - - - @Override - public void initialize(ServiceExtensionContext context) { - credentialsDir = context.getConfig().getString("edc.mvd.credentials.path"); - monitor = context.getMonitor().withPrefix("DEMO"); - } - - @Override - public void start() { - try { - seedCredentials(credentialsDir, monitor); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void seedCredentials(String credentialsSourceDirectory, Monitor monitor) throws IOException { - - var absPath = new File(credentialsSourceDirectory).getAbsoluteFile(); - - if (!absPath.exists()) { - monitor.warning("Path '%s' does not exist. It must be a resolvable path with read access. Will not add any VCs.".formatted(credentialsSourceDirectory)); - return; - } - var files = absPath.listFiles(); - if (files == null) { - monitor.warning("No files found in directory '%s'. Will not add any VCs.".formatted(credentialsSourceDirectory)); - return; - } - - var objectMapper = typeManager.getMapper(JSON_LD); - // filtering for *.json files is advised, because on K8s there can be softlinks, if a directory is mapped via ConfigMap - Stream.of(files).filter(f -> f.getName().endsWith(".json")).forEach(p -> { - try { - store.create(objectMapper.readValue(p, VerifiableCredentialResource.class)); - monitor.debug("Stored VC from file '%s'".formatted(p.getAbsolutePath())); - } catch (IOException e) { - monitor.severe("Error storing VC", e); - } - }); - } -} diff --git a/launchers/identity-hub/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/launchers/identity-hub/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension deleted file mode 100644 index 5e7628e85..000000000 --- a/launchers/identity-hub/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright (c) 2024 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 -# -# - -org.eclipse.edc.demo.dcp.ih.IdentityHubExtension \ No newline at end of file diff --git a/launchers/issuerservice/build.gradle.kts b/launchers/issuerservice/build.gradle.kts index 8a1126053..ff867c947 100644 --- a/launchers/issuerservice/build.gradle.kts +++ b/launchers/issuerservice/build.gradle.kts @@ -21,15 +21,11 @@ plugins { dependencies { implementation(libs.edc.issuance.spi) // for seeding the attestations - runtimeOnly(project(":extensions:superuser-seed")) runtimeOnly(libs.edc.bom.issuerservice) runtimeOnly(libs.edc.ih.api.did) runtimeOnly(libs.edc.ih.api.participants) - if (project.properties.getOrDefault("persistence", "false") == "true") { - runtimeOnly(libs.edc.vault.hashicorp) - runtimeOnly(libs.edc.bom.issuerservice.sql) - println("This runtime compiles with a remote STS client, Hashicorp Vault and PostgreSQL. You will need properly configured Postgres and HCV instances.") - } + runtimeOnly(libs.edc.vault.hashicorp) + runtimeOnly(libs.edc.bom.issuerservice.sql) } tasks.shadowJar { diff --git a/launchers/issuerservice/src/main/docker/Dockerfile b/launchers/issuerservice/src/main/docker/Dockerfile index 7d81d066e..b903209ef 100644 --- a/launchers/issuerservice/src/main/docker/Dockerfile +++ b/launchers/issuerservice/src/main/docker/Dockerfile @@ -14,9 +14,6 @@ COPY ${JAR} issuerservice.jar EXPOSE 8188 -ENV WEB_HTTP_PORT="8080" -ENV WEB_HTTP_PATH="/api" - HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8080/api/check/health # Use "exec" for graceful termination (SIGINT) to reach JVM. diff --git a/launchers/runtime-embedded/build.gradle.kts b/launchers/runtime-embedded/build.gradle.kts deleted file mode 100644 index 272a45c97..000000000 --- a/launchers/runtime-embedded/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -/* -* 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-library` - id("application") - alias(libs.plugins.shadow) -} - -dependencies { - runtimeOnly(project(":launchers:controlplane")) { - // this will remove the RemoteDataPlaneSelectorService - exclude(group = "org.eclipse.edc", "data-plane-selector-client") - } - runtimeOnly(project(":launchers:dataplane")) { - // this will remove the RemoteDataPlaneSelectorService - exclude(group = "org.eclipse.edc", "data-plane-selector-client") - } -} - -tasks.shadowJar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - mergeServiceFiles() - archiveFileName.set("${project.name}.jar") -} - -application { - mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") -} - -edcBuild { - publish.set(false) -} diff --git a/seed.sh b/seed.sh deleted file mode 100755 index 133170d1e..000000000 --- a/seed.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash - -# -# Copyright (c) 2024 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 -# -# - -## This script must be executed when running the dataspace from IntelliJ. Neglecting to do that will render the connectors -## inoperable! - -## Seed asset/policy/contract-def data to both "provider-qna" and "provider-manufacturing" -for url in 'http://127.0.0.1:8191' 'http://127.0.0.1:8291' -do - newman run \ - --folder "Seed" \ - --env-var "HOST=$url" \ - ./deployment/postman/MVD.postman_collection.json > /dev/null -done - -## Seed linked assets to Catalog Server -newman run \ - --folder "Seed Catalog Server" \ - --env-var "HOST=http://127.0.0.1:8091" \ - --env-var "PROVIDER_QNA_DSP_URL=http://localhost:8192" \ - --env-var "PROVIDER_MF_DSP_URL=http://localhost:8292" \ - ./deployment/postman/MVD.postman_collection.json > /dev/null - - -## Seed identity data to identityhubs -API_KEY="c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=" - -# add participant "consumer" -echo -echo -echo "Create consumer participant context in IdentityHub" -PEM_CONSUMER=$(sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' deployment/assets/consumer_public.pem) -DATA_CONSUMER=$(jq -n --arg pem "$PEM_CONSUMER" '{ - "roles":[], - "serviceEndpoints":[ - { - "type": "CredentialService", - "serviceEndpoint": "http://localhost:7081/api/credentials/v1/participants/ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgz", - "id": "consumer-credentialservice-1" - }, - { - "type": "ProtocolEndpoint", - "serviceEndpoint": "http://localhost:8082/api/dsp", - "id": "consumer-dsp" - } - ], - "active": true, - "participantId": "did:web:localhost%3A7083", - "did": "did:web:localhost%3A7083", - "key":{ - "keyId": "did:web:localhost%3A7083#key-1", - "privateKeyAlias": "key-1", - "publicKeyPem":"\($pem)" - } - }') - -# the consumer runtime will need to have the client_secret in its vault as well, so we store it in a variable -# and use the Secrets API (part of Management API) to insert it. -clientSecret=$(curl -s --location 'http://localhost:7082/api/identity/v1alpha/participants/' \ ---header 'Content-Type: application/json' \ ---header "x-api-key: $API_KEY" \ ---data "$DATA_CONSUMER" | jq -r '.clientSecret') - -# add client secret to the consumer runtime -SECRETS_DATA=$(jq -n --arg secret "$clientSecret" \ -'{ - "@context" : { - "edc" : "https://w3id.org/edc/v0.0.1/ns/" - }, - "@type" : "https://w3id.org/edc/v0.0.1/ns/Secret", - "@id" : "did:web:localhost%3A7083-sts-client-secret", - "https://w3id.org/edc/v0.0.1/ns/value": "\($secret)" -}') - -curl -sL -X POST http://localhost:8081/api/management/v3/secrets -H "x-api-key: password" -H "Content-Type: application/json" -d "$SECRETS_DATA" - -# add participant "provider" -echo -echo -echo "Create provider participant context in IdentityHub" -PEM_PROVIDER=$(sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' deployment/assets/provider_public.pem) -DATA_PROVIDER=$(jq -n --arg pem "$PEM_PROVIDER" '{ - "roles":[], - "serviceEndpoints":[ - { - "type": "CredentialService", - "serviceEndpoint": "http://localhost:7091/api/credentials/v1/participants/ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDkz", - "id": "provider-credentialservice-1" - }, - { - "type": "ProtocolEndpoint", - "serviceEndpoint": "http://localhost:8092/api/dsp", - "id": "provider-catalogserver-dsp" - } - ], - "active": true, - "participantId": "did:web:localhost%3A7093", - "did": "did:web:localhost%3A7093", - "key":{ - "keyId": "did:web:localhost%3A7093#key-1", - "privateKeyAlias": "key-1", - "publicKeyPem":"\($pem)" - } - }') - -# the provider runtime will need to have the client_secret in its vault as well, so we store it in a variable -# and use the Secrets API (part of Management API) to insert it. -clientSecret=$(curl -s --location 'http://localhost:7092/api/identity/v1alpha/participants/' \ ---header 'Content-Type: application/json' \ ---header "x-api-key: $API_KEY" \ ---data "$DATA_PROVIDER" | jq -r '.clientSecret') - -# add client secret to the provider runtimes -SECRETS_DATA=$(jq -n --arg secret "$clientSecret" \ -'{ - "@context" : { - "edc" : "https://w3id.org/edc/v0.0.1/ns/" - }, - "@type" : "https://w3id.org/edc/v0.0.1/ns/Secret", - "@id" : "did:web:localhost%3A7093-sts-client-secret", - "https://w3id.org/edc/v0.0.1/ns/value": "\($secret)" -}') - -curl -sL -X POST http://localhost:8091/api/management/v3/secrets -H "x-api-key: password" -H "Content-Type: application/json" -d "$SECRETS_DATA" -curl -sL -X POST http://localhost:8191/api/management/v3/secrets -H "x-api-key: password" -H "Content-Type: application/json" -d "$SECRETS_DATA" -curl -sL -X POST http://localhost:8291/api/management/v3/secrets -H "x-api-key: password" -H "Content-Type: application/json" -d "$SECRETS_DATA" - -############################################### -# SEED ISSUER SERVICE -############################################### - -echo -echo -echo "Create dataspace issuer" -PEM_ISSUER=$(sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' deployment/assets/issuer_public.pem) -DATA_ISSUER=$(jq -n --arg pem "$PEM_ISSUER" '{ - "roles":["admin"], - "serviceEndpoints":[ - { - "type": "IssuerService", - "serviceEndpoint": "http://localhost:10012/api/issuance/v1alpha/participants/ZGlkOndlYjpsb2NhbGhvc3QlM0ExMDEwMA==", - "id": "issuer-service-1" - } - ], - "active": true, - "participantId": "did:web:localhost%3A10100", - "did": "did:web:localhost%3A10100", - "key":{ - "keyId": "did:web:localhost%3A10100#key-1", - "privateKeyAlias": "key-1", - "keyGeneratorParams":{ - "algorithm": "EdDSA" - } - } - }') - -curl -s --location 'http://localhost:10015/api/identity/v1alpha/participants/' \ ---header 'Content-Type: application/json' \ ---data "$DATA_ISSUER" - -## Seed participant data to the issuer service -newman run \ - --folder "Seed Issuer" \ - --env-var "ISSUER_ADMIN_URL=http://localhost:10013" \ - --env-var "CONSUMER_ID=did:web:localhost%3A7083" \ - --env-var "CONSUMER_NAME=MVD Consumer Participant" \ - --env-var "PROVIDER_ID=did:web:localhost%3A7093" \ - --env-var "PROVIDER_NAME=MVD Provider Participant" \ - ./deployment/postman/MVD.postman_collection.json \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 14941bd15..5354c678b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,17 +25,12 @@ pluginManagement { } rootProject.name = "mvd" -include(":extensions:did-example-resolver") -include(":extensions:catalog-node-resolver") -include(":extensions:dcp-impl") -include(":extensions:superuser-seed") -//include(":tests:performance") include(":tests:end2end") +include(":extensions:data-plane-public-api-v2") + // launcher modules include(":launchers:identity-hub") include(":launchers:controlplane") include(":launchers:dataplane") -include(":launchers:runtime-embedded") -include(":launchers:catalog-server") include(":launchers:issuerservice") diff --git a/tests/end2end/build.gradle.kts b/tests/end2end/build.gradle.kts index 56713b03c..2781eac85 100644 --- a/tests/end2end/build.gradle.kts +++ b/tests/end2end/build.gradle.kts @@ -23,10 +23,7 @@ dependencies { testImplementation(libs.parsson) testImplementation(libs.restAssured) testImplementation(libs.awaitility) - testImplementation(libs.edc.fc.core) // todo: use 2025 once it is used everywhere - // testImplementation(libs.edc.fc.core2025) - testImplementation(libs.edc.fc.core08) testImplementation(libs.edc.lib.transform) testImplementation(libs.edc.lib.jsonld) testImplementation(libs.edc.controlplane.transform) diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java index 0adab1814..1cf52b6c8 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java @@ -17,10 +17,6 @@ import io.restassured.specification.RequestSpecification; import jakarta.json.Json; import jakarta.json.JsonArray; -import org.eclipse.edc.catalog.transform.JsonObjectToCatalogTransformer; -import org.eclipse.edc.catalog.transform.JsonObjectToDataServiceTransformer; -import org.eclipse.edc.catalog.transform.JsonObjectToDatasetTransformer; -import org.eclipse.edc.catalog.transform.JsonObjectToDistributionTransformer; import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; import org.eclipse.edc.connector.controlplane.transform.odrl.OdrlTransformersFactory; @@ -79,10 +75,10 @@ private static RequestSpecification baseRequest() { @BeforeEach void setup() { var typeManager = new JacksonTypeManager(); - transformerRegistry.register(new JsonObjectToCatalogTransformer()); - transformerRegistry.register(new JsonObjectToDatasetTransformer()); - transformerRegistry.register(new JsonObjectToDataServiceTransformer()); - transformerRegistry.register(new JsonObjectToDistributionTransformer()); +// transformerRegistry.register(new JsonObjectToCatalogTransformer()); +// transformerRegistry.register(new JsonObjectToDatasetTransformer()); +// transformerRegistry.register(new JsonObjectToDataServiceTransformer()); +// transformerRegistry.register(new JsonObjectToDistributionTransformer()); transformerRegistry.register(new JsonValueToGenericTypeTransformer(typeManager, JSON_LD)); OdrlTransformersFactory.jsonObjectToOdrlTransformers(new ParticipantIdMapper() { @Override diff --git a/values.yaml b/values.yaml new file mode 100644 index 000000000..2ca8b02ca --- /dev/null +++ b/values.yaml @@ -0,0 +1,46 @@ +# +# 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 +# + +logs: + access: + enabled: true + format: json + fields: + general: + # full field list + # https://doc.traefik.io/traefik/reference/install-configuration/observability/logs-and-accesslogs/#json-format-fields + names: + RouterName: drop +ports: + web: + port: 80 + websecure: + port: 443 + exposedPort: 443 + +# we enable gateway-api features +providers: + kubernetesGateway: + enabled: true + experimentalChannel: true + kubernetesCRD: + # -- Load Kubernetes IngressRoute provider + enabled: true + +# we disable gateway-api defaults because we want to provide our own +gatewayClass: + enabled: false + +# we disable gateway-api defaults because we want to provide our own +gateway: + enabled: false \ No newline at end of file From d5987724036170db9a02f596e6676bc3f36e4421 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 25 Mar 2026 16:34:56 +0100 Subject: [PATCH 02/22] add provider manifests --- .../application/controlplane-config.yaml | 68 ++++++++++ k8s/provider/application/controlplane.yaml | 112 +++++++++++++++ .../application/identityhub-config.yaml | 55 ++++++++ k8s/provider/application/identityhub.yaml | 128 ++++++++++++++++++ k8s/provider/base/gateway-class.yaml | 20 +++ k8s/provider/base/gateway.yaml | 31 +++++ k8s/provider/base/namespace.yaml | 17 +++ k8s/provider/base/postgres.yaml | 83 ++++++++++++ k8s/provider/base/vault.yaml | 80 +++++++++++ k8s/provider/kustomization.yml | 23 ++++ 10 files changed, 617 insertions(+) create mode 100644 k8s/provider/application/controlplane-config.yaml create mode 100644 k8s/provider/application/controlplane.yaml create mode 100644 k8s/provider/application/identityhub-config.yaml create mode 100644 k8s/provider/application/identityhub.yaml create mode 100644 k8s/provider/base/gateway-class.yaml create mode 100644 k8s/provider/base/gateway.yaml create mode 100644 k8s/provider/base/namespace.yaml create mode 100644 k8s/provider/base/postgres.yaml create mode 100644 k8s/provider/base/vault.yaml create mode 100644 k8s/provider/kustomization.yml diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml new file mode 100644 index 000000000..1c3947d00 --- /dev/null +++ b/k8s/provider/application/controlplane-config.yaml @@ -0,0 +1,68 @@ +# +# 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 +# + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: controlplane-config + namespace: provider +data: + # base config + edc.hostname: "controlplane.consumer.svc.cluster.local" + edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + edc.participant.id: "did:web:connector" + edc.iam.did.web.use.https: "false" + edc.iam.credential.revocation.mimetype: "application/json" + + # web config + web.http.port: "8080" + web.http.path: "/api" + web.http.protocol.port: "8082" + web.http.protocol.path: "/api/dsp" + web.http.management.port: "8081" + web.http.management.path: "/api/mgmt" + web.http.control.port: "8083" + web.http.control.path: "/api/control" + + # dataplane config + edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/controlplane" + edc.datasource.default.user: "cp" + edc.datasource.default.password: "cp" + edc.sql.schema.autocreate: "true" + + edc.encryption.aes.key.alias: "aes-key-alias" + + # Oauth2 config + # KeyCloak takes the `iss` claim's host from the request URL. For now, this is the URL defined in the ingress route. + # to do this properly, we should probably configure the following properties on the ingress route: + # proxy_set_header Host $host; + # proxy_set_header X-Forwarded-Proto $scheme; + edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" + + # Default scopes config + edc.iam.dcp.scopes.membership.id: "membership-scope" + edc.iam.dcp.scopes.membership.type: "DEFAULT" + edc.iam.dcp.scopes.membership.value: "org.eclipse.edc.vc.type:MembershipCredential:read" + + edc.iam.dcp.scopes.manufacturer.id: "manufacturer-scope" + edc.iam.dcp.scopes.manufacturer.type: "POLICY" + edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.edc.vc.type:ManufacturerCredential:read" + edc.iam.dcp.scopes.manufacturer.prefix-mapping: "ManufacturerCredential" + + # Trusted Issuers + edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.consumer.svc.cluster.local%3A10016:issuer" + + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" \ No newline at end of file diff --git a/k8s/provider/application/controlplane.yaml b/k8s/provider/application/controlplane.yaml new file mode 100644 index 000000000..c34695f39 --- /dev/null +++ b/k8s/provider/application/controlplane.yaml @@ -0,0 +1,112 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controlplane + namespace: provider + labels: + app: controlplane + type: edc-app +spec: + replicas: 1 + selector: + matchLabels: + app: controlplane + template: + metadata: + name: controlplane + labels: + app: controlplane + platform: edcv + type: edc-app + spec: + containers: + - name: controlplane + image: ghcr.io/eclipse-edc/mvd/controlplane:latest + imagePullPolicy: Never + envFrom: + - configMapRef: { name: controlplane-config } + ports: + - containerPort: 1044 + name: debug-port + livenessProbe: + httpGet: + path: /api/check/liveness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + startupProbe: + httpGet: + path: /api/check/startup + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + restartPolicy: Always + +--- +apiVersion: v1 +kind: Service +metadata: + name: controlplane + namespace: provider +spec: + type: ClusterIP + selector: + app: controlplane + ports: + - name: health + port: 8080 + targetPort: 8080 + - name: management + port: 8081 + targetPort: 8081 + - name: protocol + port: 8082 + targetPort: 8082 + - name: debug + port: 1044 + targetPort: 1044 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: controlplane + namespace: provider +spec: + parentRefs: + - name: consumer-gateway + kind: Gateway + sectionName: http + hostnames: + - cp.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: controlplane + port: 8081 + weight: 1 \ No newline at end of file diff --git a/k8s/provider/application/identityhub-config.yaml b/k8s/provider/application/identityhub-config.yaml new file mode 100644 index 000000000..3d4811246 --- /dev/null +++ b/k8s/provider/application/identityhub-config.yaml @@ -0,0 +1,55 @@ +# +# 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 +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: ih-config + namespace: provider +data: + edc.hostname: "identityhub.consumer.svc.cluster.local" + edc.iam.credentia.revocation.mimetype: "application/json" + edc.iam.did.web.use.https: "false" + edc.ih.iam.publickey.alias: "publickey-alias" + web.http.port: "7080" + web.http.path: "/api" + web.http.identity.port: "7081" + web.http.identity.path: "/api/identity" + web.http.identity.auth.key: "password" + web.http.credentials.port: "7082" + web.http.credentials.path: "/api/credentials" + web.http.did.port: "7083" + web.http.did.path: "/" + web.http.sts.port: "7084" + web.http.sts.path: "/api/sts" + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" + + edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + + edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/identityhub" + edc.datasource.default.user: "ih" + edc.datasource.default.password: "ih" + edc.sql.schema.autocreate: "true" + edc.iam.accesstoken.jti.validation: "true" + # grace period for credential expiry, 3600*24 = 1 day + edc.iam.credential.renewal.graceperiod: "86400" + + edc.encryption.aes.key.alias: "aes-key-alias" + # Oauth2 config + # KeyCloak takes the `iss` claim's host from the request URL. For now, this is the URL defined in the ingress route. + # to do this properly, we should probably configure the following properties on the ingress route: + # proxy_set_header Host $host; + # proxy_set_header X-Forwarded-Proto $scheme; + edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml new file mode 100644 index 000000000..c7c2986b9 --- /dev/null +++ b/k8s/provider/application/identityhub.yaml @@ -0,0 +1,128 @@ +# +# Copyright (c) 2026 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 +# + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: identityhub + namespace: provider + labels: + app: identityhub + type: edc-app +spec: + replicas: 1 + selector: + matchLabels: + app: identityhub + template: + metadata: + labels: + app: identityhub + type: edc-app + spec: + containers: + - name: identityhub + image: ghcr.io/eclipse-edc/mvd/identity-hub:latest + imagePullPolicy: Never + envFrom: + - configMapRef: + name: ih-config + ports: + - containerPort: 1044 + name: debug + livenessProbe: + httpGet: + path: /api/check/liveness + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + startupProbe: + httpGet: + path: /api/check/startup + port: 7080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + +--- +apiVersion: v1 +kind: Service +metadata: + name: identityhub + namespace: provider +spec: + type: ClusterIP + selector: + app: identityhub + ports: + - port: 7082 + targetPort: 7082 + name: creds-port + - port: 1044 + targetPort: 1044 + name: debug + - port: 7081 + targetPort: 7081 + name: identity-api + - port: 7083 + targetPort: 7083 + name: did + - port: 7084 + targetPort: 7084 + name: sts + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: identityhub + namespace: provider +spec: + parentRefs: + - name: edcv-gateway + kind: Gateway + sectionName: http + hostnames: + - ih.localhost + rules: + - matches: + - path: + type: PathPrefix + value: /cs + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: identityhub + port: 7081 + + # DID Route + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: identityhub + port: 7083 \ No newline at end of file diff --git a/k8s/provider/base/gateway-class.yaml b/k8s/provider/base/gateway-class.yaml new file mode 100644 index 000000000..2a209380e --- /dev/null +++ b/k8s/provider/base/gateway-class.yaml @@ -0,0 +1,20 @@ +# +# 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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: traefik +spec: + controllerName: traefik.io/gateway-controller \ No newline at end of file diff --git a/k8s/provider/base/gateway.yaml b/k8s/provider/base/gateway.yaml new file mode 100644 index 000000000..5454bda5e --- /dev/null +++ b/k8s/provider/base/gateway.yaml @@ -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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: consumer-gateway + namespace: provider +spec: + gatewayClassName: traefik + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All #or Same or Selector +# kinds: +# - kind: HTTPRoute +# group: gateway.networking.k8s.io \ No newline at end of file diff --git a/k8s/provider/base/namespace.yaml b/k8s/provider/base/namespace.yaml new file mode 100644 index 000000000..12d6276e0 --- /dev/null +++ b/k8s/provider/base/namespace.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: v1 +kind: Namespace +metadata: + name: provider diff --git a/k8s/provider/base/postgres.yaml b/k8s/provider/base/postgres.yaml new file mode 100644 index 000000000..669280082 --- /dev/null +++ b/k8s/provider/base/postgres.yaml @@ -0,0 +1,83 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: provider + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + platform: edcv + type: edc-infra + spec: + containers: + - name: postgres + image: postgres:17.7-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "controlplane" + - name: POSTGRES_USER + value: "cp" + - name: POSTGRES_PASSWORD + value: "cp" + volumeMounts: + - name: init-script + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init-script + configMap: + name: postgres-init +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: provider +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-init + namespace: provider +data: + init.sql: | + CREATE DATABASE identityhub; + CREATE USER ih WITH PASSWORD 'ih'; + GRANT ALL PRIVILEGES ON DATABASE identityhub TO ih; + \c identityhub + GRANT ALL ON SCHEMA public TO ih; + + CREATE DATABASE dataplane; + CREATE USER dp WITH PASSWORD 'dp'; + GRANT ALL PRIVILEGES ON DATABASE dataplane TO dp; + \c dataplane + GRANT ALL ON SCHEMA public TO dp; \ No newline at end of file diff --git a/k8s/provider/base/vault.yaml b/k8s/provider/base/vault.yaml new file mode 100644 index 000000000..49a8ee451 --- /dev/null +++ b/k8s/provider/base/vault.yaml @@ -0,0 +1,80 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vault + namespace: provider + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: vault + template: + metadata: + labels: + app: vault + type: edc-infra + spec: + containers: + - name: vault + image: hashicorp/vault:latest + ports: + - containerPort: 8200 + env: + - name: VAULT_DEV_ROOT_TOKEN_ID + value: "root" + - name: VAULT_DEV_LISTEN_ADDRESS + value: "0.0.0.0:8200" + args: + - "server" + - "-dev" + securityContext: + capabilities: + add: + - IPC_LOCK +--- +apiVersion: v1 +kind: Service +metadata: + name: vault + namespace: provider +spec: + selector: + app: vault + ports: + - port: 8200 + targetPort: 8200 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: vault + namespace: provider +spec: + parentRefs: + - name: consumer-gateway + hostnames: + - vault.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: vault + port: 8200 \ No newline at end of file diff --git a/k8s/provider/kustomization.yml b/k8s/provider/kustomization.yml new file mode 100644 index 000000000..59a1599c5 --- /dev/null +++ b/k8s/provider/kustomization.yml @@ -0,0 +1,23 @@ +# +# Copyright (c) 2026 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 +# + +resources: + - base/namespace.yaml + - base/vault.yaml + - base/gateway.yaml + - base/gateway-class.yaml + - base/postgres.yaml + - application/controlplane-config.yaml + - application/controlplane.yaml + - application/identityhub-config.yaml + - application/identityhub.yaml \ No newline at end of file From a41b1934306026390af6b85c0f2370d3f3bdbe59 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 25 Mar 2026 16:42:55 +0100 Subject: [PATCH 03/22] fix http routes --- k8s/consumer/application/controlplane.yaml | 2 +- k8s/consumer/application/identityhub.yaml | 2 +- k8s/consumer/base/vault.yaml | 2 +- k8s/provider/application/controlplane-config.yaml | 12 ++++++------ k8s/provider/application/controlplane.yaml | 2 +- k8s/provider/application/identityhub-config.yaml | 10 +++++----- k8s/provider/application/identityhub.yaml | 2 +- k8s/provider/base/vault.yaml | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/k8s/consumer/application/controlplane.yaml b/k8s/consumer/application/controlplane.yaml index 7be42a8d6..f0bbd8490 100644 --- a/k8s/consumer/application/controlplane.yaml +++ b/k8s/consumer/application/controlplane.yaml @@ -100,7 +100,7 @@ spec: kind: Gateway sectionName: http hostnames: - - cp.localhost + - cp.consumer.localhost rules: - matches: - path: diff --git a/k8s/consumer/application/identityhub.yaml b/k8s/consumer/application/identityhub.yaml index 3b290f946..8d303d308 100644 --- a/k8s/consumer/application/identityhub.yaml +++ b/k8s/consumer/application/identityhub.yaml @@ -102,7 +102,7 @@ spec: kind: Gateway sectionName: http hostnames: - - ih.localhost + - ih.consumer.localhost rules: - matches: - path: diff --git a/k8s/consumer/base/vault.yaml b/k8s/consumer/base/vault.yaml index 3e3579fa6..595d36382 100644 --- a/k8s/consumer/base/vault.yaml +++ b/k8s/consumer/base/vault.yaml @@ -69,7 +69,7 @@ spec: parentRefs: - name: consumer-gateway hostnames: - - vault.localhost + - vault.consumer.localhost rules: - matches: - path: diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml index 1c3947d00..eb1db546f 100644 --- a/k8s/provider/application/controlplane-config.yaml +++ b/k8s/provider/application/controlplane-config.yaml @@ -19,8 +19,8 @@ metadata: namespace: provider data: # base config - edc.hostname: "controlplane.consumer.svc.cluster.local" - edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.hostname: "controlplane.provider.svc.cluster.local" + edc.vault.hashicorp.url: "http://vault.provider.svc.cluster.local:8200" edc.vault.hashicorp.token: "root" edc.participant.id: "did:web:connector" edc.iam.did.web.use.https: "false" @@ -37,7 +37,7 @@ data: web.http.control.path: "/api/control" # dataplane config - edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/controlplane" + edc.datasource.default.url: "jdbc:postgresql://postgres.provider.svc.cluster.local:5432/controlplane" edc.datasource.default.user: "cp" edc.datasource.default.password: "cp" edc.sql.schema.autocreate: "true" @@ -49,8 +49,8 @@ data: # to do this properly, we should probably configure the following properties on the ingress route: # proxy_set_header Host $host; # proxy_set_header X-Forwarded-Proto $scheme; - edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" - edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" + edc.iam.oauth2.issuer: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" # Default scopes config edc.iam.dcp.scopes.membership.id: "membership-scope" @@ -63,6 +63,6 @@ data: edc.iam.dcp.scopes.manufacturer.prefix-mapping: "ManufacturerCredential" # Trusted Issuers - edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.consumer.svc.cluster.local%3A10016:issuer" + edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.provider.svc.cluster.local%3A10016:issuer" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" \ No newline at end of file diff --git a/k8s/provider/application/controlplane.yaml b/k8s/provider/application/controlplane.yaml index c34695f39..4742c07dd 100644 --- a/k8s/provider/application/controlplane.yaml +++ b/k8s/provider/application/controlplane.yaml @@ -100,7 +100,7 @@ spec: kind: Gateway sectionName: http hostnames: - - cp.localhost + - cp.provider.localhost rules: - matches: - path: diff --git a/k8s/provider/application/identityhub-config.yaml b/k8s/provider/application/identityhub-config.yaml index 3d4811246..124705f80 100644 --- a/k8s/provider/application/identityhub-config.yaml +++ b/k8s/provider/application/identityhub-config.yaml @@ -17,7 +17,7 @@ metadata: name: ih-config namespace: provider data: - edc.hostname: "identityhub.consumer.svc.cluster.local" + edc.hostname: "identityhub.provider.svc.cluster.local" edc.iam.credentia.revocation.mimetype: "application/json" edc.iam.did.web.use.https: "false" edc.ih.iam.publickey.alias: "publickey-alias" @@ -34,10 +34,10 @@ data: web.http.sts.path: "/api/sts" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" - edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" + edc.vault.hashicorp.url: "http://vault.provider.svc.cluster.local:8200" edc.vault.hashicorp.token: "root" - edc.datasource.default.url: "jdbc:postgresql://postgres.consumer.svc.cluster.local:5432/identityhub" + edc.datasource.default.url: "jdbc:postgresql://postgres.provider.svc.cluster.local:5432/identityhub" edc.datasource.default.user: "ih" edc.datasource.default.password: "ih" edc.sql.schema.autocreate: "true" @@ -51,5 +51,5 @@ data: # to do this properly, we should probably configure the following properties on the ingress route: # proxy_set_header Host $host; # proxy_set_header X-Forwarded-Proto $scheme; - edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" - edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file + edc.iam.oauth2.issuer: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml index c7c2986b9..5b1f42378 100644 --- a/k8s/provider/application/identityhub.yaml +++ b/k8s/provider/application/identityhub.yaml @@ -102,7 +102,7 @@ spec: kind: Gateway sectionName: http hostnames: - - ih.localhost + - ih.provider.localhost rules: - matches: - path: diff --git a/k8s/provider/base/vault.yaml b/k8s/provider/base/vault.yaml index 49a8ee451..47ac3a3a2 100644 --- a/k8s/provider/base/vault.yaml +++ b/k8s/provider/base/vault.yaml @@ -69,7 +69,7 @@ spec: parentRefs: - name: consumer-gateway hostnames: - - vault.localhost + - vault.provider.localhost rules: - matches: - path: From 06cb30ab7b60a4dbbaafe4454215461737f287b8 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 25 Mar 2026 17:24:00 +0100 Subject: [PATCH 04/22] add issuer deployment --- gradle/libs.versions.toml | 5 +- .../application/issuerservice-config.yaml | 67 ++++ .../application/issuerservice-seed-job.yaml | 318 ++++++++++++++++++ k8s/issuer/application/issuerservice.yaml | 155 +++++++++ k8s/issuer/base/gateway-class.yaml | 20 ++ k8s/issuer/base/gateway.yaml | 31 ++ k8s/issuer/base/keycloak.yaml | 311 +++++++++++++++++ k8s/issuer/base/namespace.yaml | 17 + k8s/issuer/base/postgres.yaml | 83 +++++ k8s/issuer/base/vault.yaml | 211 ++++++++++++ k8s/issuer/kustomization.yml | 10 + launchers/issuerservice/build.gradle.kts | 2 + .../{ => demo}/DemoAttestationSource.java | 2 +- .../DemoAttestationSourceFactory.java | 2 +- .../{ => demo}/DemoAttestationsExtension.java | 4 +- .../DemoAttestatonSourceValidator.java | 2 +- .../ManufacturerAttestationExtension.java | 46 +++ .../ManufacturerAttestationSource.java | 38 +++ .../ManufacturerAttestationSourceFactory.java | 27 ++ ...anufacturerAttestationSourceValidator.java | 26 ++ .../MembershipAttestationSource.java | 33 ++ .../MembershipAttestationSourceFactory.java | 28 ++ .../MembershipAttestationSourceValidator.java | 26 ++ .../MembershipAttestationsExtension.java | 48 +++ ...rg.eclipse.edc.spi.system.ServiceExtension | 4 +- 25 files changed, 1508 insertions(+), 8 deletions(-) create mode 100644 k8s/issuer/application/issuerservice-config.yaml create mode 100644 k8s/issuer/application/issuerservice-seed-job.yaml create mode 100644 k8s/issuer/application/issuerservice.yaml create mode 100644 k8s/issuer/base/gateway-class.yaml create mode 100644 k8s/issuer/base/gateway.yaml create mode 100644 k8s/issuer/base/keycloak.yaml create mode 100644 k8s/issuer/base/namespace.yaml create mode 100644 k8s/issuer/base/postgres.yaml create mode 100644 k8s/issuer/base/vault.yaml create mode 100644 k8s/issuer/kustomization.yml rename launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/{ => demo}/DemoAttestationSource.java (93%) rename launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/{ => demo}/DemoAttestationSourceFactory.java (93%) rename launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/{ => demo}/DemoAttestationsExtension.java (89%) rename launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/{ => demo}/DemoAttestatonSourceValidator.java (92%) create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationExtension.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSource.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceFactory.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceValidator.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSource.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceFactory.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceValidator.java create mode 100644 launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationsExtension.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a49a7999..b87e26221 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,7 @@ edc-sql-lease = { module = "org.eclipse.edc:sql-lease", version.ref = "edc" } edc-sql-pool = { module = "org.eclipse.edc:sql-pool-apache-commons", version.ref = "edc" } edc-sql-transactionlocal = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-sql-dataplane-instancestore = { module = "org.eclipse.edc:data-plane-instance-store-sql", version.ref = "edc" } +edc-store-participantcontext-config-sql = { module = "org.eclipse.edc:participantcontext-config-store-sql", version.ref = "edc" } # identityhub SPI modules edc-ih-spi-did = { module = "org.eclipse.edc:did-spi", version.ref = "edc" } @@ -106,9 +107,9 @@ edc-bom-controlplane = { module = "org.eclipse.edc:controlplane-dcp-bom", versio edc-bom-dataplane = { module = "org.eclipse.edc:dataplane-base-bom", version.ref = "edc" } edc-bom-controlplane-sql = { module = "org.eclipse.edc:controlplane-feature-sql-bom", version.ref = "edc" } edc-bom-dataplane-sql = { module = "org.eclipse.edc:dataplane-feature-sql-bom", version.ref = "edc" } -edc-bom-identityhub = { module = "org.eclipse.edc:identityhub-bom", version.ref = "edc" } +edc-bom-identityhub = { module = "org.eclipse.edc:identityhub-oauth2-bom", version.ref = "edc" } edc-bom-identityhub-sql = { module = "org.eclipse.edc:identityhub-feature-sql-bom", version.ref = "edc" } -edc-bom-issuerservice = { module = "org.eclipse.edc:issuerservice-bom", version.ref = "edc" } +edc-bom-issuerservice = { module = "org.eclipse.edc:issuerservice-oauth2-bom", version.ref = "edc" } edc-bom-issuerservice-sql = { module = "org.eclipse.edc:issuerservice-feature-sql-bom", version.ref = "edc" } [bundles] diff --git a/k8s/issuer/application/issuerservice-config.yaml b/k8s/issuer/application/issuerservice-config.yaml new file mode 100644 index 000000000..382fa53e7 --- /dev/null +++ b/k8s/issuer/application/issuerservice-config.yaml @@ -0,0 +1,67 @@ +# +# 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 +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: issuerservice-config + namespace: issuer +data: + # kv format + + edc.hostname: "issuerservice.issuer.svc.cluster.local" + edc.issuer.statuslist.signing.key.alias: "statuslist-signing-key" + edc.ih.api.superuser.key: "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=" + edc.iam.did.web.use.https: "false" + # Web config + web.http.port: "10010" + web.http.path: "/api" + web.http.sts.port: "10011" + web.http.sts.path: "/api/sts" + web.http.issuance.port: "10012" + web.http.issuance.path: "/api/issuance" + web.http.issueradmin.port: "10013" + web.http.issueradmin.path: "/api/admin" + web.http.identity.port: "10015" + web.http.identity.path: "/api/identity" + web.http.did.port: "10016" + web.http.did.path: "/" + web.http.statuslist.port: "9999" + web.http.statuslist.path: "/statuslist" + + # persistence and vault + edc.vault.hashicorp.url: "http://vault.issuer.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + + edc.datasource.default.url: "jdbc:postgresql://postgres.issuer.svc.cluster.local:5432/issuerservice" + edc.datasource.default.user: "issuer" + edc.datasource.default.password: "issuer" + edc.sql.schema.autocreate: "true" + edc.iam.accesstoken.jti.validation: "true" + + edc.encryption.aes.key.alias: "aes-key-alias" + # Oauth2 config + # KeyCloak takes the `iss` claim's host from the request URL. For now, this is the URL defined in the ingress route. + # to do this properly, we should probably configure the following properties on the ingress route: + # proxy_set_header Host $host; + # proxy_set_header X-Forwarded-Proto $scheme; + edc.iam.oauth2.issuer: "http://keycloak.issuer.svc.cluster.local:8080/realms/mvd" + edc.iam.oauth2.jwks.url: "http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" + + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" + + # even though we have a default data source, we need a named datasource for the DatabaseAttestationSource, because + # that is configured in the AttestationDefinition + edc.datasource.membership.url: "jdbc:postgresql://postgres.issuer.svc.cluster.local:5432/issuerservice" + edc.datasource.membership.user: "issuer" + edc.datasource.membership.password: "issuer" \ No newline at end of file diff --git a/k8s/issuer/application/issuerservice-seed-job.yaml b/k8s/issuer/application/issuerservice-seed-job.yaml new file mode 100644 index 000000000..15a89e369 --- /dev/null +++ b/k8s/issuer/application/issuerservice-seed-job.yaml @@ -0,0 +1,318 @@ +# +# 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 +# + +## This seed job creates a tenant for the "issuer" in the IssuerService, as well as creates an Attestation Definition and +## Credential Definition. + +apiVersion: batch/v1 +kind: Job +metadata: + name: issuerservice-seed + namespace: issuer + labels: + app: issuerservice-seed + platform: edcv + type: edcv-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: issuerservice-seed + platform: edcv + type: edcv-job + spec: + restartPolicy: OnFailure + initContainers: + # Wait for issuerservice to be ready + - name: wait-for-issuerservice + image: curlimages/curl:latest + command: + - sh + - -c + - | + until curl -sf http://issuerservice.issuer.svc.cluster.local:10010/api/check/readiness; do + echo "Waiting for issuerservice to be ready..." + sleep 5 + done + echo "" + echo "IssuerService is ready!" + + # Wait for Keycloak to be ready + echo "Waiting for Keycloak to be ready..." + until wget -q --spider http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + containers: + - name: seed-issuerservice + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.issuer.svc.cluster.local:8080" + - name: ISSUER_CLIENT_ID + value: "issuer" + - name: ISSUER_CLIENT_SECRET + value: "issuer-secret" + command: + - sh + - -c + - | + set -e + + echo "================================================" + echo "Step 1: Create Vault Access Token (Issuer)" + echo "================================================" + + # Get Keycloak admin token + KC_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "username=admin" \ + -d "password=admin" \ + -d "client_id=admin-cli" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$KC_TOKEN" ]; then + echo "Failed to get Keycloak admin token" + exit 1 + fi + + # Create Keycloak client for Vault access + echo "Creating Vault Access Client" + if curl -sf -X POST "${KC_HOST}/admin/realms/mvd/clients" \ + -H "Authorization: Bearer ${KC_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "'"${ISSUER_CLIENT_ID}"'", + "name": "Issuer Client", + "description": "Client for Vault Access (Issuer)", + "enabled": true, + "secret": "'"${ISSUER_CLIENT_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": "issuer", + "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" + } + } + ] + }'; then + echo "✓ Vault Access Token created" + else + echo "⚠ Vault Access Client creation failed (may already exist)" + fi + + echo "" + echo "================================================" + echo "Step 2: Create Issuer Tenant in IssuerService" + echo "================================================" + + # Get provisioner token + PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=provisioner" \ + -d "client_secret=provisioner-secret" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$PROVISIONER_TOKEN" ]; then + echo "Failed to get provisioner token" + exit 1 + fi + + # Create issuer tenant + TENANT_RESPONSE=$(curl -sf -X POST "http://issuerservice.issuer.svc.cluster.local:10015/api/identity/v1alpha/participants" \ + -H "Authorization: Bearer ${PROVISIONER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "roles": ["admin"], + "serviceEndpoints": [ + { + "type": "IssuerService", + "serviceEndpoint": "http://issuerservice.issuer.svc.cluster.local:10012/api/issuance/v1alpha/participants/issuer", + "id": "issuer-service-1" + } + ], + "active": true, + "participantContextId": "issuer", + "did": "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer", + "key": { + "keyId": "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer#key-1", + "privateKeyAlias": "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer#key-1", + "keyGeneratorParams": { + "algorithm": "EdDSA" + } + }, + "additionalProperties": { + "edc.vault.hashicorp.config": { + "credentials": { + "clientId": "'"${ISSUER_CLIENT_ID}"'", + "clientSecret": "'"${ISSUER_CLIENT_SECRET}"'", + "tokenUrl": "http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/token" + }, + "config": { + "secretPath": "v1/participants", + "folderPath": "'"${ISSUER_CLIENT_ID}"'", + "vaultUrl": "http://vault.issuer.svc.cluster.local:8200" + } + } + } + }') + + echo "✓ Issuer tenant created" + + echo "" + echo "================================================" + echo "Step 3: Create AttestationDefinitions" + echo "================================================" + + # Get issuer token + ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=issuer" \ + -d "client_secret=issuer-secret" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ISSUER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + # Create attestation definitions + echo "Creating Membership AttestationDefinition" + curl -sfS -w "\nHTTP_STATUS:%{http_code}\n" -X POST "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha/participants/issuer/attestations" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "attestationType": "membership", + "configuration": {}, + "id": "membership-attestation-def-1" + }' + + echo "Creating Manufacturer AttestationDefinition" + curl -sfS -w "\nHTTP_STATUS:%{http_code}\n" -X POST "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha/participants/issuer/attestations" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "attestationType": "manufacturer", + "configuration": {}, + "id": "manufacturer-attestation-def-1" + }' + + echo "✓ AttestationDefinitions created" + + echo "" + echo "================================================" + echo "Step 4: Create CredentialDefinitions" + echo "================================================" + + # Create credential definitions + echo "Creating Membership CredentialDefinition" + curl -sfS -w "\nHTTP_STATUS:%{http_code}\n" -X POST "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha/participants/issuer/credentialdefinitions" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "attestations": ["membership-attestation-def-1"], + "credentialType": "MembershipCredential", + "id": "membership-credential-def", + "jsonSchema": "{}", + "jsonSchemaUrl": "https://example.com/schema/membership-credential.json", + "mappings": [ + { + "input": "membership", + "output": "credentialSubject.membership", + "required": true + }, + { + "input": "membershipType", + "output": "credentialSubject.membershipType", + "required": "true" + }, + { + "input": "membershipStartDate", + "output": "credentialSubject.membershipStartDate", + "required": true + } + ], + "rules": [], + "format": "VC1_0_JWT", + "validity": "604800" + }' + + echo "Creating Manufacturer CredentialDefinition" + curl -sfS -w "\nHTTP_STATUS:%{http_code}\n" -X POST "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha/participants/issuer/credentialdefinitions" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "attestations": ["manufacturer-attestation-def-1"], + "credentialType": "ManufacturerCredential", + "id": "manufacturer-credential-def", + "jsonSchema": "{}", + "jsonSchemaUrl": "https://example.com/schema/manufacturer-credential.json", + "mappings": [ + { + "input": "contractVersion", + "output": "credentialSubject.contractVersion", + "required": true + }, + { + "input": "component_types", + "output": "credentialSubject.part_types", + "required": "true" + }, + { + "input": "since", + "output": "credentialSubject.since", + "required": true + } + ], + "rules": [], + "format": "VC1_0_JWT", + "validity": "604800" + }' + + echo "✓ CredentialDefinition created" + echo "" + echo "================================================" + echo "IssuerService seeding completed successfully!" + echo "================================================" diff --git a/k8s/issuer/application/issuerservice.yaml b/k8s/issuer/application/issuerservice.yaml new file mode 100644 index 000000000..0b0253afc --- /dev/null +++ b/k8s/issuer/application/issuerservice.yaml @@ -0,0 +1,155 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: issuerservice + namespace: issuer + labels: + app: issuerservice + type: edc-app +spec: + replicas: 1 + selector: + matchLabels: + app: issuerservice + template: + metadata: + name: issuerservice + labels: + app: issuerservice + type: edc-app + spec: + containers: + - name: issuerservice + image: ghcr.io/eclipse-edc/mvd/issuerservice:latest + imagePullPolicy: Never + ports: + - containerPort: 80 + protocol: TCP + envFrom: + - configMapRef: { name: issuerservice-config } + livenessProbe: + httpGet: + path: /api/check/liveness + port: 10010 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 10010 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + startupProbe: + httpGet: + path: /api/check/startup + port: 10010 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 120 + restartPolicy: Always + + +--- +apiVersion: v1 +kind: Service +metadata: + name: issuerservice + namespace: issuer +spec: + type: ClusterIP + selector: + app: issuerservice + ports: + - port: 10010 + targetPort: 10010 + name: web + - port: 10011 + targetPort: 10011 + name: sts + - port: 10012 + targetPort: 10012 + name: issuance + - port: 10013 + targetPort: 10013 + name: issueradmin + - port: 10015 + targetPort: 10015 + name: identity + - port: 10016 + targetPort: 10016 + name: did + - port: 9999 + targetPort: 9999 + name: statuslist + + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: issuerservice + namespace: issuer +spec: + parentRefs: + - name: edcv-gateway + kind: Gateway + sectionName: http + hostnames: + - issuer.localhost + rules: + # Issuer Admin API + - matches: + - path: + type: PathPrefix + value: /admin + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: issuerservice + port: 10013 + weight: 1 + + # Credential Service API + - matches: + - path: + type: PathPrefix + value: /cs + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: issuerservice + port: 10015 + weight: 1 + + # Web DID Resolution endpoint + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: issuerservice + port: 10016 + weight: 1 \ No newline at end of file diff --git a/k8s/issuer/base/gateway-class.yaml b/k8s/issuer/base/gateway-class.yaml new file mode 100644 index 000000000..2a209380e --- /dev/null +++ b/k8s/issuer/base/gateway-class.yaml @@ -0,0 +1,20 @@ +# +# 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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: traefik +spec: + controllerName: traefik.io/gateway-controller \ No newline at end of file diff --git a/k8s/issuer/base/gateway.yaml b/k8s/issuer/base/gateway.yaml new file mode 100644 index 000000000..87bd76e40 --- /dev/null +++ b/k8s/issuer/base/gateway.yaml @@ -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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: issuer-gateway + namespace: issuer +spec: + gatewayClassName: traefik + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All #or Same or Selector +# kinds: +# - kind: HTTPRoute +# group: gateway.networking.k8s.io \ No newline at end of file diff --git a/k8s/issuer/base/keycloak.yaml b/k8s/issuer/base/keycloak.yaml new file mode 100644 index 000000000..0ddbf1e77 --- /dev/null +++ b/k8s/issuer/base/keycloak.yaml @@ -0,0 +1,311 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + namespace: issuer +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + type: edc-infra + spec: + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:latest + args: + - "start-dev" + - "--health-enabled=true" + - "--import-realm" + ports: + - containerPort: 8080 + name: http + - containerPort: 9000 + name: health + env: + - name: KEYCLOAK_ADMIN + value: "admin" + - name: KC_BOOTSTRAP_ADMIN_USERNAME + value: "admin" + - name: KC_BOOTSTRAP_ADMIN_PASSWORD + value: "admin" + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_HOSTNAME + value: http://keycloak.issuer.svc.cluster.local:8080 + - name: KC_HOSTNAME_STRICT + value: "false" + - name: KC_HOSTNAME_URL + value: "http://keycloak.issuer.svc.cluster.local:8080" + - name: KC_PROXY + value: "edge" + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_METRICS_ENABLED + value: "true" + - name: KC_LOG_LEVEL + value: INFO + - name: KC_DB + value: postgres + - name: POSTGRES_DB + value: keycloak + - name: KC_DB_URL + value: jdbc:postgresql://postgres.issuer.svc.cluster.local:5432/keycloak + - name: KC_DB_USERNAME + value: "kc" + - name: KC_DB_PASSWORD + value: "kc" + volumeMounts: + - name: realm-config + mountPath: /opt/keycloak/data/import + readinessProbe: + httpGet: + path: /health/ready + port: 9000 + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 12 + livenessProbe: + httpGet: + path: /health/live + port: 9000 + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 12 + volumes: + - name: realm-config + configMap: + name: keycloak-realm +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-realm + namespace: issuer + labels: + tier: infrastructure +data: + mvd-realm.json: | + { + "realm": "mvd", + "enabled": true, + "sslRequired": "none", + "displayName": "MVD Realm", + "roles": { + "realm": [ + { + "name": "admin", + "description": "Administrator with full access to all tenants and their resources"}, + { + "name": "provisioner", + "description": "Can create and delete tenants but cannot access their resources"}, + { + "name": "participant", + "description": "Regular participant context access role, can access their own resources"} + ] + }, + "clientScopes": [ + { + "name": "identity-api:read", + "description": "Read access to Identity API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "identity-api:write", + "description": "Create, update delete objects in the Identity API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "management-api:read", + "description": "Read access to the Management API of the EDC Control Plane", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "management-api:write", + "description": "Create, update and delete objects in the Management API of the EDC Control Plane", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "issuer-admin-api:read", + "description": "read access to the Issuer Admin API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "issuer-admin-api:write", + "description": "write access to the Issuer Admin API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + } + ], + "defaultOptionalClientScopes": [ + "offline_access", + "management-api:read", + "management-api:write", + "identity-api:read", + "identity-api:write", + "issuer-admin-api:read", + "issuer-admin-api:write" + ], + "clients": [ + { + "clientId": "admin", + "name": "EDC-V Admin User", + "description": "Global admin client with full access", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "secret": "edc-v-admin-secret", + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "fullScopeAllowed": true, + "protocolMappers": [ + { + "name": "role", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "role", + "claim.value": "admin", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "issuer-admin-api:write", + "issuer-admin-api:read", + "identity-api:write", + "identity-api:read", + "management-api:write", + "management-api:read" + ] + }, + { + "clientId": "provisioner", + "name": "Provisioner User", + "description": "Can create and delete tenants", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "secret": "provisioner-secret", + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "fullScopeAllowed": true, + "protocolMappers": [ + { + "name": "role", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "role", + "claim.value": "provisioner", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "issuer-admin-api:write", + "issuer-admin-api:read", + "identity-api:write", + "identity-api:read", + "management-api:write", + "management-api:read" + ] + } + ], + "users": [], + "groups": [], + "eventsEnabled": false + } + +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak + namespace: issuer + labels: + tier: infrastructure +spec: + selector: + app: keycloak + ports: + - port: 8080 + targetPort: 8080 + name: http + - port: 9000 + targetPort: 9000 + name: health + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: keycloak + namespace: issuer +spec: + parentRefs: + - name: issuer-gateway + kind: Gateway + sectionName: http + hostnames: + - keycloak.issuer.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: keycloak + port: 8080 \ No newline at end of file diff --git a/k8s/issuer/base/namespace.yaml b/k8s/issuer/base/namespace.yaml new file mode 100644 index 000000000..e8b967f3f --- /dev/null +++ b/k8s/issuer/base/namespace.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: v1 +kind: Namespace +metadata: + name: issuer diff --git a/k8s/issuer/base/postgres.yaml b/k8s/issuer/base/postgres.yaml new file mode 100644 index 000000000..9fd6e5fc5 --- /dev/null +++ b/k8s/issuer/base/postgres.yaml @@ -0,0 +1,83 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: issuer + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + platform: edcv + type: edc-infra + spec: + containers: + - name: postgres + image: postgres:17.7-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "controlplane" + - name: POSTGRES_USER + value: "cp" + - name: POSTGRES_PASSWORD + value: "cp" + volumeMounts: + - name: init-script + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init-script + configMap: + name: postgres-init +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: issuer +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-init + namespace: issuer +data: + init.sql: | + CREATE DATABASE issuerservice; + CREATE USER issuer WITH PASSWORD 'issuer'; + GRANT ALL PRIVILEGES ON DATABASE issuerservice TO issuer; + \c issuerservice + GRANT ALL ON SCHEMA public TO issuer; + + CREATE DATABASE keycloak; + CREATE USER kc WITH PASSWORD 'kc'; + GRANT ALL PRIVILEGES ON DATABASE keycloak TO kc; + \c keycloak + GRANT ALL ON SCHEMA public TO kc; \ No newline at end of file diff --git a/k8s/issuer/base/vault.yaml b/k8s/issuer/base/vault.yaml new file mode 100644 index 000000000..556cb4c23 --- /dev/null +++ b/k8s/issuer/base/vault.yaml @@ -0,0 +1,211 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vault + namespace: issuer + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: vault + template: + metadata: + labels: + app: vault + type: edc-infra + spec: + containers: + - name: vault + image: hashicorp/vault:latest + ports: + - containerPort: 8200 + env: + - name: VAULT_DEV_ROOT_TOKEN_ID + value: "root" + - name: VAULT_DEV_LISTEN_ADDRESS + value: "0.0.0.0:8200" + args: + - "server" + - "-dev" + securityContext: + capabilities: + add: + - IPC_LOCK + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: vault-auth + namespace: issuer + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: vault-bootstrap + namespace: issuer +spec: + backoffLimit: 10 + template: + metadata: + labels: + type: edc-job + spec: + serviceAccountName: vault-auth + containers: + - name: vault-cli + image: hashicorp/vault:latest + env: + - name: VAULT_ADDR + value: "http://vault.issuer.svc.cluster.local:8200" + - name: VAULT_TOKEN + value: "root" + command: [ "sh", "-ec" ] + args: + - | + # Wait for vault to be ready + echo "Waiting for Vault to be ready..." + until vault status > /dev/null 2>&1; do + echo "Vault not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Vault is ready!" + + # Wait for Keycloak to be ready + echo "Waiting for Keycloak to be ready..." + until wget -q --spider http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + + # Enable JWT auth method + vault auth enable jwt || true + + # Configure JWT auth (example: using Keycloak as JWT backend) + vault write auth/jwt/config \ + jwks_url="http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ + default_role="participant" || { echo "Failed to configure JWT auth"; exit 1; } + + # create mount for participants, each stores their secrets in a subdirectory + vault secrets enable -path=participants -version=2 kv || { echo "Failed to enable secrets engine"; exit 1; } + + # get accessor for entity aliases + ACCESSOR=$(vault auth list | grep 'jwt/' | awk '{print $3}') + if [ -z "$ACCESSOR" ]; then + echo "Failed to get JWT accessor" + exit 1 + fi + echo "Using JWT accessor: $ACCESSOR" + + + # Allow full CRUD + list on the participant's own data path + + cat < config) implements AttestationSource { + private static final String DEFAULT_CONTRACT_VERSION = "1.0.0"; + + @Override + public Result> execute(AttestationContext context) { + var contractVersion = config.getOrDefault("contractVersion", DEFAULT_CONTRACT_VERSION); + + return Result.success(Map.of( + "contractVersion", contractVersion, + "component_types", "all", + "since", Instant.now().toString(), + "id", context.participantContextId() + )); + } +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceFactory.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceFactory.java new file mode 100644 index 000000000..29047ce5b --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceFactory.java @@ -0,0 +1,27 @@ +/* + * 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.issuerservice.seed.attestation.manufacturer; + +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSource; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactory; +import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; + +public class ManufacturerAttestationSourceFactory implements AttestationSourceFactory { + @Override + public AttestationSource createSource(AttestationDefinition definition) { + var config = definition.getConfiguration(); + return new ManufacturerAttestationSource(config); + } +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceValidator.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceValidator.java new file mode 100644 index 000000000..13518c836 --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSourceValidator.java @@ -0,0 +1,26 @@ +/* + * 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.issuerservice.seed.attestation.manufacturer; + +import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +public class ManufacturerAttestationSourceValidator implements Validator { + @Override + public ValidationResult validate(AttestationDefinition input) { + return ValidationResult.success(); + } +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSource.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSource.java new file mode 100644 index 000000000..cbfa7f394 --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSource.java @@ -0,0 +1,33 @@ +/* + * 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.issuerservice.seed.attestation.membership; + +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSource; +import org.eclipse.edc.spi.result.Result; + +import java.time.Instant; +import java.util.Map; + +public class MembershipAttestationSource implements AttestationSource { + @Override + public Result> execute(AttestationContext attestationContext) { + return Result.success(Map.of( + "membership", Map.of("since", Instant.now().toString()), + "membershipType", "full-member", + "membershipStartDate", Instant.now().toString(), + "id", attestationContext.participantContextId())); + } +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceFactory.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceFactory.java new file mode 100644 index 000000000..3e2573375 --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceFactory.java @@ -0,0 +1,28 @@ +/* + * 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.issuerservice.seed.attestation.membership; + +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSource; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactory; +import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; + +public class MembershipAttestationSourceFactory implements AttestationSourceFactory { + @Override + public AttestationSource createSource(AttestationDefinition definition) { + var config = definition.getConfiguration(); + return new MembershipAttestationSource(); + } + +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceValidator.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceValidator.java new file mode 100644 index 000000000..fd40c41e4 --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationSourceValidator.java @@ -0,0 +1,26 @@ +/* + * 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.issuerservice.seed.attestation.membership; + +import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +public class MembershipAttestationSourceValidator implements Validator { + @Override + public ValidationResult validate(AttestationDefinition attestationDefinition) { + return ValidationResult.success(); + } +} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationsExtension.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationsExtension.java new file mode 100644 index 000000000..86884aa00 --- /dev/null +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/membership/MembershipAttestationsExtension.java @@ -0,0 +1,48 @@ +/* + * 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.issuerservice.seed.attestation.membership; + +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionValidatorRegistry; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactoryRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static org.eclipse.edc.issuerservice.seed.attestation.membership.MembershipAttestationsExtension.NAME; + + +@Extension(value = NAME) +public class MembershipAttestationsExtension implements ServiceExtension { + + public static final String NAME = "Membership Attestations Extension"; + + @Inject + private AttestationSourceFactoryRegistry registry; + + @Inject + private AttestationDefinitionValidatorRegistry validatorRegistry; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + registry.registerFactory("membership", new MembershipAttestationSourceFactory()); + validatorRegistry.registerValidator("membership", new MembershipAttestationSourceValidator()); + } +} diff --git a/launchers/issuerservice/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/launchers/issuerservice/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index e5057b13a..0c6d46b9a 100644 --- a/launchers/issuerservice/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/launchers/issuerservice/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -11,4 +11,6 @@ # Cofinity-X - initial API and implementation # # -org.eclipse.edc.issuerservice.seed.attestation.DemoAttestationsExtension \ No newline at end of file +org.eclipse.edc.issuerservice.seed.attestation.demo.DemoAttestationsExtension +org.eclipse.edc.issuerservice.seed.attestation.manufacturer.ManufacturerAttestationExtension +org.eclipse.edc.issuerservice.seed.attestation.membership.MembershipAttestationsExtension From 4d83fe3005822b5f74719787adcd2df3a5172cd4 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 07:05:22 +0100 Subject: [PATCH 05/22] add common infra --- .../base => common}/gateway-class.yaml | 0 k8s/common/kustomization.yaml | 2 ++ k8s/consumer/kustomization.yml | 1 - k8s/issuer/base/gateway-class.yaml | 20 ------------------- k8s/issuer/kustomization.yml | 1 - k8s/kustomization.yml | 5 +++++ k8s/provider/base/gateway-class.yaml | 20 ------------------- k8s/provider/kustomization.yml | 1 - 8 files changed, 7 insertions(+), 43 deletions(-) rename k8s/{consumer/base => common}/gateway-class.yaml (100%) create mode 100644 k8s/common/kustomization.yaml delete mode 100644 k8s/issuer/base/gateway-class.yaml create mode 100644 k8s/kustomization.yml delete mode 100644 k8s/provider/base/gateway-class.yaml diff --git a/k8s/consumer/base/gateway-class.yaml b/k8s/common/gateway-class.yaml similarity index 100% rename from k8s/consumer/base/gateway-class.yaml rename to k8s/common/gateway-class.yaml diff --git a/k8s/common/kustomization.yaml b/k8s/common/kustomization.yaml new file mode 100644 index 000000000..2962779f6 --- /dev/null +++ b/k8s/common/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - gateway-class.yaml \ No newline at end of file diff --git a/k8s/consumer/kustomization.yml b/k8s/consumer/kustomization.yml index 59a1599c5..bc066afcd 100644 --- a/k8s/consumer/kustomization.yml +++ b/k8s/consumer/kustomization.yml @@ -15,7 +15,6 @@ resources: - base/namespace.yaml - base/vault.yaml - base/gateway.yaml - - base/gateway-class.yaml - base/postgres.yaml - application/controlplane-config.yaml - application/controlplane.yaml diff --git a/k8s/issuer/base/gateway-class.yaml b/k8s/issuer/base/gateway-class.yaml deleted file mode 100644 index 2a209380e..000000000 --- a/k8s/issuer/base/gateway-class.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright (c) 2025 Metaform Systems, Inc. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# -# Contributors: -# Metaform Systems, Inc. - initial API and implementation -# - ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: GatewayClass -metadata: - name: traefik -spec: - controllerName: traefik.io/gateway-controller \ No newline at end of file diff --git a/k8s/issuer/kustomization.yml b/k8s/issuer/kustomization.yml index a91f9bb28..6674630aa 100644 --- a/k8s/issuer/kustomization.yml +++ b/k8s/issuer/kustomization.yml @@ -2,7 +2,6 @@ resources: - base/namespace.yaml - base/vault.yaml - base/gateway.yaml - - base/gateway-class.yaml - base/postgres.yaml - base/keycloak.yaml - application/issuerservice-config.yaml diff --git a/k8s/kustomization.yml b/k8s/kustomization.yml new file mode 100644 index 000000000..fb0255234 --- /dev/null +++ b/k8s/kustomization.yml @@ -0,0 +1,5 @@ +resources: + - common + - ./consumer + - ./provider + - ./issuer \ No newline at end of file diff --git a/k8s/provider/base/gateway-class.yaml b/k8s/provider/base/gateway-class.yaml deleted file mode 100644 index 2a209380e..000000000 --- a/k8s/provider/base/gateway-class.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright (c) 2025 Metaform Systems, Inc. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# -# Contributors: -# Metaform Systems, Inc. - initial API and implementation -# - ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: GatewayClass -metadata: - name: traefik -spec: - controllerName: traefik.io/gateway-controller \ No newline at end of file diff --git a/k8s/provider/kustomization.yml b/k8s/provider/kustomization.yml index 59a1599c5..bc066afcd 100644 --- a/k8s/provider/kustomization.yml +++ b/k8s/provider/kustomization.yml @@ -15,7 +15,6 @@ resources: - base/namespace.yaml - base/vault.yaml - base/gateway.yaml - - base/gateway-class.yaml - base/postgres.yaml - application/controlplane-config.yaml - application/controlplane.yaml From 299b010dfa4060aa73d37d85bc4b4c33915ed23b Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 08:18:44 +0100 Subject: [PATCH 06/22] use common keycloak --- k8s/{issuer/base => common}/keycloak.yaml | 76 ++++------ k8s/common/kustomization.yaml | 5 +- k8s/common/namespace.yaml | 17 +++ k8s/common/postgres.yaml | 76 ++++++++++ k8s/consumer/base/vault.yaml | 130 +++++++++++++++++ .../application/issuerservice-config.yaml | 4 +- .../application/issuerservice-seed-job.yaml | 12 +- k8s/issuer/base/vault.yaml | 4 +- k8s/issuer/kustomization.yml | 1 - k8s/provider/base/vault.yaml | 131 ++++++++++++++++++ 10 files changed, 396 insertions(+), 60 deletions(-) rename k8s/{issuer/base => common}/keycloak.yaml (83%) create mode 100644 k8s/common/namespace.yaml create mode 100644 k8s/common/postgres.yaml diff --git a/k8s/issuer/base/keycloak.yaml b/k8s/common/keycloak.yaml similarity index 83% rename from k8s/issuer/base/keycloak.yaml rename to k8s/common/keycloak.yaml index 0ddbf1e77..216f8487d 100644 --- a/k8s/issuer/base/keycloak.yaml +++ b/k8s/common/keycloak.yaml @@ -15,7 +15,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: keycloak - namespace: issuer + namespace: mvd-common spec: replicas: 1 selector: @@ -49,11 +49,11 @@ spec: - name: KC_HTTP_ENABLED value: "true" - name: KC_HOSTNAME - value: http://keycloak.issuer.svc.cluster.local:8080 + value: http://keycloak.mvd-common.svc.cluster.local:8080 - name: KC_HOSTNAME_STRICT value: "false" - name: KC_HOSTNAME_URL - value: "http://keycloak.issuer.svc.cluster.local:8080" + value: "http://keycloak.mvd-common.svc.cluster.local:8080" - name: KC_PROXY value: "edge" - name: KC_HEALTH_ENABLED @@ -67,7 +67,7 @@ spec: - name: POSTGRES_DB value: keycloak - name: KC_DB_URL - value: jdbc:postgresql://postgres.issuer.svc.cluster.local:5432/keycloak + value: jdbc:postgresql://postgres.mvd-common.svc.cluster.local:5432/keycloak - name: KC_DB_USERNAME value: "kc" - name: KC_DB_PASSWORD @@ -100,7 +100,7 @@ apiVersion: v1 kind: ConfigMap metadata: name: keycloak-realm - namespace: issuer + namespace: mvd-common labels: tier: infrastructure data: @@ -142,24 +142,6 @@ data: "display.on.consent.screen": "true" } }, - { - "name": "management-api:read", - "description": "Read access to the Management API of the EDC Control Plane", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true" - } - }, - { - "name": "management-api:write", - "description": "Create, update and delete objects in the Management API of the EDC Control Plane", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true" - } - }, { "name": "issuer-admin-api:read", "description": "read access to the Issuer Admin API", @@ -221,9 +203,7 @@ data: "issuer-admin-api:write", "issuer-admin-api:read", "identity-api:write", - "identity-api:read", - "management-api:write", - "management-api:read" + "identity-api:read" ] }, { @@ -274,7 +254,7 @@ apiVersion: v1 kind: Service metadata: name: keycloak - namespace: issuer + namespace: mvd-common labels: tier: infrastructure spec: @@ -288,24 +268,24 @@ spec: targetPort: 9000 name: health ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: keycloak - namespace: issuer -spec: - parentRefs: - - name: issuer-gateway - kind: Gateway - sectionName: http - hostnames: - - keycloak.issuer.localhost - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: keycloak - port: 8080 \ No newline at end of file +#--- +#apiVersion: gateway.networking.k8s.io/v1 +#kind: HTTPRoute +#metadata: +# name: keycloak +# namespace: mvd-common +#spec: +# parentRefs: +# - name: issuer-gateway +# kind: Gateway +# sectionName: http +# hostnames: +# - keycloak.issuer.localhost +# rules: +# - matches: +# - path: +# type: PathPrefix +# value: / +# backendRefs: +# - name: keycloak +# port: 8080 \ No newline at end of file diff --git a/k8s/common/kustomization.yaml b/k8s/common/kustomization.yaml index 2962779f6..0056c0646 100644 --- a/k8s/common/kustomization.yaml +++ b/k8s/common/kustomization.yaml @@ -1,2 +1,5 @@ resources: - - gateway-class.yaml \ No newline at end of file + - namespace.yaml + - gateway-class.yaml + - postgres.yaml + - keycloak.yaml \ No newline at end of file diff --git a/k8s/common/namespace.yaml b/k8s/common/namespace.yaml new file mode 100644 index 000000000..aee2d8754 --- /dev/null +++ b/k8s/common/namespace.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2026 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 +# + +apiVersion: v1 +kind: Namespace +metadata: + name: mvd-common diff --git a/k8s/common/postgres.yaml b/k8s/common/postgres.yaml new file mode 100644 index 000000000..90a89b035 --- /dev/null +++ b/k8s/common/postgres.yaml @@ -0,0 +1,76 @@ +# +# 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 +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: mvd-common + labels: + type: edc-infra +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + platform: edcv + type: edc-infra + spec: + containers: + - name: postgres + image: postgres:17.7-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "keycloak" + - name: POSTGRES_USER + value: "kc" + - name: POSTGRES_PASSWORD + value: "kc" + volumeMounts: + - name: init-script + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init-script + configMap: + name: postgres-init +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: mvd-common +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-init + namespace: mvd-common +data: + init.sql: | + + GRANT ALL PRIVILEGES ON DATABASE keycloak TO kc; + \c keycloak + GRANT ALL ON SCHEMA public TO kc; \ No newline at end of file diff --git a/k8s/consumer/base/vault.yaml b/k8s/consumer/base/vault.yaml index 595d36382..bf85b6e3b 100644 --- a/k8s/consumer/base/vault.yaml +++ b/k8s/consumer/base/vault.yaml @@ -46,6 +46,136 @@ spec: capabilities: add: - IPC_LOCK + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: vault-auth + namespace: consumer + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: vault-bootstrap + namespace: consumer +spec: + backoffLimit: 10 + template: + metadata: + labels: + type: edc-job + spec: + serviceAccountName: vault-auth + containers: + - name: vault-cli + image: hashicorp/vault:latest + env: + - name: VAULT_ADDR + value: "http://vault.consumer.svc.cluster.local:8200" + - name: VAULT_TOKEN + value: "root" + command: [ "sh", "-ec" ] + args: + - | + # Wait for vault to be ready + echo "Waiting for Vault to be ready..." + until vault status > /dev/null 2>&1; do + echo "Vault not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Vault is ready!" + + # Wait for Keycloak to be ready + echo "Waiting for Keycloak to be ready..." + until wget -q --spider http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + + # Enable JWT auth method + vault auth enable jwt || true + + # Configure JWT auth (example: using Keycloak as JWT backend) + vault write auth/jwt/config \ + jwks_url="http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ + default_role="participant" || { echo "Failed to configure JWT auth"; exit 1; } + + # create mount for participants, each stores their secrets in a subdirectory + vault secrets enable -path=participants -version=2 kv || { echo "Failed to enable secrets engine"; exit 1; } + + # get accessor for entity aliases + ACCESSOR=$(vault auth list | grep 'jwt/' | awk '{print $3}') + if [ -z "$ACCESSOR" ]; then + echo "Failed to get JWT accessor" + exit 1 + fi + echo "Using JWT accessor: $ACCESSOR" + + + # Allow full CRUD + list on the participant's own data path + + cat < /dev/null 2>&1; do + until wget -q --spider http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do echo "Keycloak not ready yet, retrying in 2 seconds..." sleep 2 done @@ -60,7 +60,7 @@ spec: image: curlimages/curl:latest env: - name: KC_HOST - value: "http://keycloak.issuer.svc.cluster.local:8080" + value: "http://keycloak.mvd-common.svc.cluster.local:8080" - name: ISSUER_CLIENT_ID value: "issuer" - name: ISSUER_CLIENT_SECRET @@ -147,7 +147,7 @@ spec: echo "================================================" # Get provisioner token - PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + PROVISIONER_TOKEN=$(curl -sfv -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=provisioner" \ @@ -160,7 +160,7 @@ spec: fi # Create issuer tenant - TENANT_RESPONSE=$(curl -sf -X POST "http://issuerservice.issuer.svc.cluster.local:10015/api/identity/v1alpha/participants" \ + TENANT_RESPONSE=$(curl -sfv -X POST "http://issuerservice.issuer.svc.cluster.local:10015/api/identity/v1alpha/participants" \ -H "Authorization: Bearer ${PROVISIONER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ @@ -187,7 +187,7 @@ spec: "credentials": { "clientId": "'"${ISSUER_CLIENT_ID}"'", "clientSecret": "'"${ISSUER_CLIENT_SECRET}"'", - "tokenUrl": "http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/token" + "tokenUrl": "http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/token" }, "config": { "secretPath": "v1/participants", @@ -206,7 +206,7 @@ spec: echo "================================================" # Get issuer token - ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + ISSUER_TOKEN=$(curl -sfv -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=issuer" \ diff --git a/k8s/issuer/base/vault.yaml b/k8s/issuer/base/vault.yaml index 556cb4c23..c1416a6c3 100644 --- a/k8s/issuer/base/vault.yaml +++ b/k8s/issuer/base/vault.yaml @@ -89,7 +89,7 @@ spec: # Wait for Keycloak to be ready echo "Waiting for Keycloak to be ready..." - until wget -q --spider http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do + until wget -q --spider http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do echo "Keycloak not ready yet, retrying in 2 seconds..." sleep 2 done @@ -100,7 +100,7 @@ spec: # Configure JWT auth (example: using Keycloak as JWT backend) vault write auth/jwt/config \ - jwks_url="http://keycloak.issuer.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ + jwks_url="http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ default_role="participant" || { echo "Failed to configure JWT auth"; exit 1; } # create mount for participants, each stores their secrets in a subdirectory diff --git a/k8s/issuer/kustomization.yml b/k8s/issuer/kustomization.yml index 6674630aa..0a8321a83 100644 --- a/k8s/issuer/kustomization.yml +++ b/k8s/issuer/kustomization.yml @@ -3,7 +3,6 @@ resources: - base/vault.yaml - base/gateway.yaml - base/postgres.yaml - - base/keycloak.yaml - application/issuerservice-config.yaml - application/issuerservice.yaml - application/issuerservice-seed-job.yaml \ No newline at end of file diff --git a/k8s/provider/base/vault.yaml b/k8s/provider/base/vault.yaml index 47ac3a3a2..b273ee4e9 100644 --- a/k8s/provider/base/vault.yaml +++ b/k8s/provider/base/vault.yaml @@ -46,6 +46,137 @@ spec: capabilities: add: - IPC_LOCK + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: vault-auth + namespace: provider + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: vault-bootstrap + namespace: provider +spec: + backoffLimit: 10 + template: + metadata: + labels: + type: edc-job + spec: + serviceAccountName: vault-auth + containers: + - name: vault-cli + image: hashicorp/vault:latest + env: + - name: VAULT_ADDR + value: "http://vault.provider.svc.cluster.local:8200" + - name: VAULT_TOKEN + value: "root" + command: [ "sh", "-ec" ] + args: + - | + # Wait for vault to be ready + echo "Waiting for Vault to be ready..." + until vault status > /dev/null 2>&1; do + echo "Vault not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Vault is ready!" + + # Wait for Keycloak to be ready + echo "Waiting for Keycloak to be ready..." + until wget -q --spider http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/.well-known/openid-configuration > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + + # Enable JWT auth method + vault auth enable jwt || true + + # Configure JWT auth (example: using Keycloak as JWT backend) + vault write auth/jwt/config \ + jwks_url="http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ + default_role="participant" || { echo "Failed to configure JWT auth"; exit 1; } + + # create mount for participants, each stores their secrets in a subdirectory + vault secrets enable -path=participants -version=2 kv || { echo "Failed to enable secrets engine"; exit 1; } + + # get accessor for entity aliases + ACCESSOR=$(vault auth list | grep 'jwt/' | awk '{print $3}') + if [ -z "$ACCESSOR" ]; then + echo "Failed to get JWT accessor" + exit 1 + fi + echo "Using JWT accessor: $ACCESSOR" + + + # Allow full CRUD + list on the participant's own data path + + cat < Date: Thu, 26 Mar 2026 11:04:33 +0100 Subject: [PATCH 07/22] add consumer seeding --- gradle/libs.versions.toml | 40 +- .../application/controlplane-seed.yaml | 108 +++++ k8s/consumer/kustomization.yml | 1 + .../application/controlplane-config.yaml | 2 + .../application/controlplane-seed.yaml | 382 ++++++++++++++++++ k8s/provider/kustomization.yml | 1 + launchers/controlplane/build.gradle.kts | 3 + .../test/resources/negotiation-request.json | 2 +- 8 files changed, 503 insertions(+), 36 deletions(-) create mode 100644 k8s/consumer/application/controlplane-seed.yaml create mode 100644 k8s/provider/application/controlplane-seed.yaml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b87e26221..42b59017a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,11 +36,12 @@ edc-controlplane-transform = { module = "org.eclipse.edc:control-plane-transform edc-controlplane-services = { module = "org.eclipse.edc:control-plane-aggregate-services", version.ref = "edc" } edc-api-management-config = { module = "org.eclipse.edc:management-api-configuration", version.ref = "edc" } edc-api-management = { module = "org.eclipse.edc:management-api", version.ref = "edc" } +edc-api-cel-v5 = {module = "org.eclipse.edc:cel-api-v5", version.ref= "edc"} edc-api-secrets = { module = "org.eclipse.edc:secrets-api", version.ref = "edc" } edc-api-observability = { module = "org.eclipse.edc:api-observability", version.ref = "edc" } edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dataplane-v2 = { module = "org.eclipse.edc:data-plane-public-api-v2", version.ref = "edc" } - +edc-core-cel = { module = "org.eclipse.edc:cel-core", version.ref = "edc" } edc-dcp-core = { module = "org.eclipse.edc:decentralized-claims-core", version.ref = "edc" } edc-vault-hashicorp = { module = "org.eclipse.edc:vault-hashicorp", version.ref = "edc" } edc-spi-identity-trust = { module = "org.eclipse.edc:decentralized-claims-spi", version.ref = "edc" } @@ -61,6 +62,8 @@ edc-lib-http = { module = "org.eclipse.edc:http-lib", version.ref = "edc" } edc-lib-util = { module = "org.eclipse.edc:util-lib", version.ref = "edc" } edc-lib-sql = { module = "org.eclipse.edc:sql-lib", version.ref = "edc" } edc-lib-util-dataplane = { module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } +edc-lib-oauth2-authn = { module = "org.eclipse.edc:auth-authentication-oauth2-lib", version.ref = "edc" } +edc-lib-oauth2-authz = { module = "org.eclipse.edc:auth-authorization-oauth2-lib", version.ref = "edc" } # EDC Postgres modules edc-sql-assetindex = { module = "org.eclipse.edc:asset-index-sql", version.ref = "edc" } @@ -75,6 +78,7 @@ edc-sql-pool = { module = "org.eclipse.edc:sql-pool-apache-commons", version.ref edc-sql-transactionlocal = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-sql-dataplane-instancestore = { module = "org.eclipse.edc:data-plane-instance-store-sql", version.ref = "edc" } edc-store-participantcontext-config-sql = { module = "org.eclipse.edc:participantcontext-config-store-sql", version.ref = "edc" } +edc-cel-store-sql = { module = "org.eclipse.edc:cel-store-sql", version.ref = "edc" } # identityhub SPI modules edc-ih-spi-did = { module = "org.eclipse.edc:did-spi", version.ref = "edc" } @@ -113,40 +117,6 @@ edc-bom-issuerservice = { module = "org.eclipse.edc:issuerservice-oauth2-bom", v edc-bom-issuerservice-sql = { module = "org.eclipse.edc:issuerservice-feature-sql-bom", version.ref = "edc" } [bundles] -connector = [ - "edc-boot", - "edc-core-connector", - "edc-core-runtime", - "edc-core-api", - "edc-ext-http", - "edc-api-observability", - "edc-ext-jsonld", - "edc-core-token", - "edc-config-fs", -] - -dcp = [ - "edc-dcp", - "edc-did-core", - "edc-did-web", - "edc-oauth2-client", - "edc-dcp-core", -] - -sql-edc = [ - "edc-sql-assetindex", - "edc-sql-contractdef", - "edc-sql-contractneg", - "edc-sql-policydef", - "edc-sql-edrcache", - "edc-sql-transferprocess", - "edc-sql-dataplane-instancestore", - "edc-sql-core", - "edc-sql-lease", - "edc-sql-pool", - "edc-sql-transactionlocal", - "postgres", -] [plugins] shadow = { id = "com.gradleup.shadow", version = "9.4.0" } diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml new file mode 100644 index 000000000..dba6cc993 --- /dev/null +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -0,0 +1,108 @@ + +--- +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: controlplane-seed-cel + namespace: consumer + labels: + app: controlplane-seed-cel + platform: edcv + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: controlplane-seed-cel + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + until curl -sf http://controlplane.consumer.svc.cluster.local:8080/api/check/readiness; do + echo "Waiting for controlplane to be ready..." + sleep 5 + done + echo "" + echo "Controlplane is ready!" + containers: + - name: seed-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + set -e + + MGMT_URL="http://controlplane.consumer.svc.cluster.local:8081/api/mgmt/v5alpha/celexpressions" + + # Posts to the management API, treating 409 (already exists) as success. + create_cel() { + HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "${MGMT_URL}" \ + -H "Content-Type: application/json" \ + -d "$1") + if [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ CEL Expression already exists (HTTP 409), skipping." + elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + exit 1 + fi + } + + echo "================================================" + echo "Step 1: Create Membership CEL Expression " + echo "================================================" + + create_cel '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CelExpression", + "@id": "membership-cel", + "leftOperand": "MembershipCredential", + "description": "Expression for evaluating membership credential", + "scopes": [ + "catalog", + "contract.negotiation", + "transfer.process" + ], + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''MembershipCredential'\'')).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))" + }' + + echo "✓ Membership CEL Expression done" + + echo "" + echo "================================================" + echo "Step 2: Create Dataproccesor CEL Expression" + echo "================================================" + + create_cel '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CelExpression", + "@id": "membership-cel", + "leftOperand": "DataProcessorCredential", + "description": "Expression for evaluating data processor credential", + "scopes": [ + "catalog", + "contract.negotiation", + "transfer.process" + ], + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''DataProcessorCredential'\'')).size() > 0" + }' + + echo "✓ Dataprocessor CEL Expression done" + + echo "" + echo "================================================" + echo "Controlplane seeding completed successfully!" + echo "================================================" + diff --git a/k8s/consumer/kustomization.yml b/k8s/consumer/kustomization.yml index bc066afcd..91af9826e 100644 --- a/k8s/consumer/kustomization.yml +++ b/k8s/consumer/kustomization.yml @@ -18,5 +18,6 @@ resources: - base/postgres.yaml - application/controlplane-config.yaml - application/controlplane.yaml + - application/controlplane-seed.yaml - application/identityhub-config.yaml - application/identityhub.yaml \ No newline at end of file diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml index eb1db546f..21647e771 100644 --- a/k8s/provider/application/controlplane-config.yaml +++ b/k8s/provider/application/controlplane-config.yaml @@ -25,6 +25,8 @@ data: edc.participant.id: "did:web:connector" edc.iam.did.web.use.https: "false" edc.iam.credential.revocation.mimetype: "application/json" + # todo: should this be true? + edc.policy.validation.enabled: "false" # web config web.http.port: "8080" diff --git a/k8s/provider/application/controlplane-seed.yaml b/k8s/provider/application/controlplane-seed.yaml new file mode 100644 index 000000000..a4be78413 --- /dev/null +++ b/k8s/provider/application/controlplane-seed.yaml @@ -0,0 +1,382 @@ +# +# Copyright (c) 2026 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 +# + +## This seed job creates assets, policies and contract definitions provider's control plane via the management API. + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: controlplane-seed-assets + namespace: provider + labels: + app: controlplane-seed-assets + platform: edcv + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: controlplane-seed-assets + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + until curl -sf http://controlplane.provider.svc.cluster.local:8080/api/check/readiness; do + echo "Waiting for controlplane to be ready..." + sleep 5 + done + echo "" + echo "Controlplane is ready!" + containers: + - name: seed-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + set -e + + MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/assets" + + # Posts to the management API, treating 409 (already exists) as success. + create_asset() { + HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "${MGMT_URL}" \ + -H "Content-Type: application/json" \ + -d "$1") + if [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Asset already exists (HTTP 409), skipping." + elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + exit 1 + fi + } + + echo "================================================" + echo "Step 1: Create Asset 1" + echo "================================================" + + create_asset '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@id": "asset-1", + "@type": "Asset", + "properties": { + "description": "This asset requires Membership to view and negotiate." + }, + "dataAddress": { + "@type": "DataAddress", + "type": "HttpData", + "baseUrl": "https://jsonplaceholder.typicode.com/todos", + "proxyPath": "true", + "proxyQueryParams": "true" + } + }' + + echo "✓ Asset 1 done" + + echo "" + echo "================================================" + echo "Step 2: Create Asset 2" + echo "================================================" + + create_asset '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@id": "asset-2", + "@type": "Asset", + "properties": { + "description": "This asset requires Membership to view and SensitiveData credential to negotiate." + }, + "dataAddress": { + "@type": "DataAddress", + "type": "HttpData", + "baseUrl": "https://jsonplaceholder.typicode.com/todos", + "proxyPath": "true", + "proxyQueryParams": "true" + } + }' + + echo "✓ Asset 2 done" + + echo "" + echo "================================================" + echo "Controlplane seeding completed successfully!" + echo "================================================" + + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: controlplane-seed-policies + namespace: provider + labels: + app: controlplane-seed-policies + platform: edcv + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: controlplane-seed-policies + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + until curl -sf http://controlplane.provider.svc.cluster.local:8080/api/check/readiness; do + echo "Waiting for controlplane to be ready..." + sleep 5 + done + echo "" + echo "Controlplane is ready!" + containers: + - name: seed-policies + image: curlimages/curl:latest + command: + - sh + - -c + - | + set -e + + MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/policydefinitions" + + # Posts a policy definition, treating 409 (already exists) as success. + create_policy() { + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${MGMT_URL}" \ + -H "Content-Type: application/json" \ + -d "$1") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Policy already exists (HTTP 409), skipping." + elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + } + + echo "================================================" + echo "Step 1: Create Membership Policy" + echo "================================================" + + create_policy '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "PolicyDefinition", + "@id": "require-membership", + "policy": { + "@type": "Set", + "permission": [ + { + "action": "use", + "constraint": { + "leftOperand": "MembershipCredential", + "operator": "eq", + "rightOperand": "active" + } + } + ] + } + }' + + echo "✓ Membership policy done" + + echo "" + echo "================================================" + echo "Step 2: Create DataProcessor Policy" + echo "================================================" + + create_policy '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "PolicyDefinition", + "@id": "require-dataprocessor", + "policy": { + "@type": "Set", + "obligation": [ + { + "action": "use", + "constraint": { + "leftOperand": "DataAccess.level", + "operator": "eq", + "rightOperand": "processing" + } + } + ] + } + }' + + echo "✓ DataProcessor policy done" + + echo "" + echo "================================================" + echo "Step 3: Create Sensitive Data Policy" + echo "================================================" + + create_policy '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "PolicyDefinition", + "@id": "require-sensitive", + "policy": { + "@type": "Set", + "permission": [], + "prohibition": [], + "obligation": [ + { + "action": "use", + "constraint": { + "leftOperand": "DataAccess.level", + "operator": "eq", + "rightOperand": "sensitive" + } + } + ] + } + }' + + echo "✓ Sensitive data policy done" + + echo "" + echo "================================================" + echo "Policy seeding completed successfully!" + echo "================================================" + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: controlplane-seed-contractdefs + namespace: provider + labels: + app: controlplane-seed-contractdefs + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: controlplane-seed-contractdefs + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-controlplane + image: curlimages/curl:latest + command: + - sh + - -c + - | + until curl -sf http://controlplane.provider.svc.cluster.local:8080/api/check/readiness; do + echo "Waiting for controlplane to be ready..." + sleep 5 + done + echo "" + echo "Controlplane is ready!" + containers: + - name: seed-contractdefs + image: curlimages/curl:latest + command: + - sh + - -c + - | + set -e + + MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/contractdefinitions" + + # Posts a contract definition, treating 409 (already exists) as success. + create_contractdef() { + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${MGMT_URL}" \ + -H "Content-Type: application/json" \ + -d "$1") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Contract definition already exists (HTTP 409), skipping." + elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + } + + echo "================================================" + echo "Step 1: Create member-and-dataprocessor-def" + echo "================================================" + + create_contractdef '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@id": "member-and-dataprocessor-def", + "@type": "ContractDefinition", + "accessPolicyId": "require-membership", + "contractPolicyId": "require-dataprocessor", + "assetsSelector": { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator": "=", + "operandRight": "asset-1" + } + }' + + echo "✓ member-and-dataprocessor-def done" + + echo "" + echo "================================================" + echo "Step 2: Create sensitive-only-def" + echo "================================================" + + create_contractdef '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@id": "sensitive-only-def", + "@type": "ContractDefinition", + "accessPolicyId": "require-membership", + "contractPolicyId": "require-sensitive", + "assetsSelector": { + "@type": "Criterion", + "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", + "operator": "=", + "operandRight": "asset-2" + } + }' + + echo "✓ sensitive-only-def done" + + echo "" + echo "================================================" + echo "Contract definition seeding completed successfully!" + echo "================================================" \ No newline at end of file diff --git a/k8s/provider/kustomization.yml b/k8s/provider/kustomization.yml index bc066afcd..91af9826e 100644 --- a/k8s/provider/kustomization.yml +++ b/k8s/provider/kustomization.yml @@ -18,5 +18,6 @@ resources: - base/postgres.yaml - application/controlplane-config.yaml - application/controlplane.yaml + - application/controlplane-seed.yaml - application/identityhub-config.yaml - application/identityhub.yaml \ No newline at end of file diff --git a/launchers/controlplane/build.gradle.kts b/launchers/controlplane/build.gradle.kts index 202648163..322b80324 100644 --- a/launchers/controlplane/build.gradle.kts +++ b/launchers/controlplane/build.gradle.kts @@ -19,6 +19,9 @@ plugins { } dependencies { + runtimeOnly(libs.edc.api.cel.v5) + runtimeOnly(libs.edc.core.cel) + runtimeOnly(libs.edc.cel.store.sql) runtimeOnly(libs.edc.bom.controlplane) runtimeOnly(libs.edc.api.secrets) runtimeOnly(libs.edc.vault.hashicorp) diff --git a/tests/end2end/src/test/resources/negotiation-request.json b/tests/end2end/src/test/resources/negotiation-request.json index 44c45d01a..15358240a 100644 --- a/tests/end2end/src/test/resources/negotiation-request.json +++ b/tests/end2end/src/test/resources/negotiation-request.json @@ -1,6 +1,6 @@ { "@context": [ - "https://w3id.org/edc/connector/management/v0.0.1" + "https://w3id.org/edc/connector/management/v2" ], "@type": "ContractRequest", "counterPartyAddress": "{{PROVIDER_DSP_URL}}/api/dsp", From a600f769040cdc13f8ce35fdbcc2c0fde02792e7 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 13:02:35 +0100 Subject: [PATCH 08/22] seed identityhub (consumer) --- .../application/controlplane-config.yaml | 4 +- .../application/controlplane-seed.yaml | 1 - .../application/identityhub-config.yaml | 4 +- .../application/identityhub-seed.yaml | 206 ++++++++++++++++++ k8s/consumer/application/identityhub.yaml | 3 + k8s/consumer/kustomization.yml | 3 +- 6 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 k8s/consumer/application/identityhub-seed.yaml diff --git a/k8s/consumer/application/controlplane-config.yaml b/k8s/consumer/application/controlplane-config.yaml index 9fd1e07a5..6827fa974 100644 --- a/k8s/consumer/application/controlplane-config.yaml +++ b/k8s/consumer/application/controlplane-config.yaml @@ -49,8 +49,8 @@ data: # to do this properly, we should probably configure the following properties on the ingress route: # proxy_set_header Host $host; # proxy_set_header X-Forwarded-Proto $scheme; - edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" - edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" + edc.iam.oauth2.issuer: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/edcv" + edc.iam.oauth2.jwks.url: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" # Default scopes config edc.iam.dcp.scopes.membership.id: "membership-scope" diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml index dba6cc993..74a3c9814 100644 --- a/k8s/consumer/application/controlplane-seed.yaml +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -1,5 +1,4 @@ ---- --- apiVersion: batch/v1 kind: Job diff --git a/k8s/consumer/application/identityhub-config.yaml b/k8s/consumer/application/identityhub-config.yaml index 5af7449c3..072366cec 100644 --- a/k8s/consumer/application/identityhub-config.yaml +++ b/k8s/consumer/application/identityhub-config.yaml @@ -51,5 +51,5 @@ data: # to do this properly, we should probably configure the following properties on the ingress route: # proxy_set_header Host $host; # proxy_set_header X-Forwarded-Proto $scheme; - edc.iam.oauth2.issuer: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv" - edc.iam.oauth2.jwks.url: "http://keycloak.consumer.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file + edc.iam.oauth2.issuer: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd" + edc.iam.oauth2.jwks.url: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ No newline at end of file diff --git a/k8s/consumer/application/identityhub-seed.yaml b/k8s/consumer/application/identityhub-seed.yaml new file mode 100644 index 000000000..56b91bcda --- /dev/null +++ b/k8s/consumer/application/identityhub-seed.yaml @@ -0,0 +1,206 @@ +# +# Copyright (c) 2026 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 +# + +## This seed job creates a participant context in the consumer's IdentityHub and a holder entry in the IssuerService. + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: identityhub-seed + namespace: consumer + labels: + app: identityhub-seed + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: identityhub-seed + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-dependencies + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + + command: + - sh + - -c + - | + until curl -sf http://identityhub.consumer.svc.cluster.local:7080/api/check/readiness; do + echo "Waiting for identityhub to be ready..." + sleep 5 + done + echo "" + echo "IdentityHub is ready!" + + until curl -sf http://issuerservice.issuer.svc.cluster.local:10010/api/check/readiness; do + echo "Waiting for issuerservice to be ready..." + sleep 5 + done + echo "" + echo "IssuerService is ready!" + + # Wait for Keycloak + echo "Waiting for Keycloak to be ready..." + until wget -q --spider "${KC_HOST}/realms/mvd/.well-known/openid-configuration" > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + containers: + - name: seed-issuerservice + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + - name: ISSUER_CLIENT_ID + value: "issuer" + - name: ISSUER_CLIENT_SECRET + value: "issuer-secret" + - name: ISSUER_ADMIN_URL + value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" + - name: PARTICIPANT_DID + value: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" + - name: PARTICIPANT_ID + value: "consumer-participant" + command: + - sh + - -c + - | + set -e + echo "" + echo "================================================" + echo "Step 1: Create Consumer Holder in IssuerService" + echo "================================================" + + ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ISSUER_CLIENT_ID}" \ + -d "client_secret=${ISSUER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ISSUER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"did\": \"${PARTICIPANT_DID}\", + \"holderId\": \"${PARTICIPANT_DID}\", + \"name\": \"MVD Consumer Participant\" + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ Consumer holder created in IssuerService" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Consumer holder already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + - name: seed-identityhub + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + - name: PROVISIONER_CLIENT_ID + value: "provisioner" + - name: PROVISIONER_CLIENT_SECRET + value: "provisioner-secret" + - name: IH_URL + value: "http://identityhub.consumer.svc.cluster.local:7081/api/identity/v1alpha/participants/" + - name: PARTICIPANT_DID + value: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" + - name: PARTICIPANT_ID + value: "consumer-participant" + command: + - sh + - -c + - | + set -e + + echo "================================================" + echo "Step 1: Create Consumer Participant Context in IdentityHub" + echo "================================================" + + PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${PROVISIONER_CLIENT_ID}" \ + -d "client_secret=${PROVISIONER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$PROVISIONER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${IH_URL}" \ + -H "Authorization: Bearer ${PROVISIONER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"roles\": [], + \"serviceEndpoints\": [ + { + \"type\": \"CredentialService\", + \"serviceEndpoint\": \"http://identityhub.consumer.svc.cluster.local:7082/api/credentials/v1/participants/${PARTICIPANT_ID}\", + \"id\": \"consumer-credentialservice-1\" + }, + { + \"type\": \"ProtocolEndpoint\", + \"serviceEndpoint\": \"http://controlplane.consumer.svc.cluster.local:8082/api/dsp\", + \"id\": \"consumer-dsp\" + } + ], + \"active\": true, + \"participantId\": \"${PARTICIPANT_DID}\", + \"participantContextId\": \"${PARTICIPANT_ID}\", + \"did\": \"${PARTICIPANT_DID}\", + \"key\": { + \"keyId\": \"${PARTICIPANT_DID}#key-1\", + \"privateKeyAlias\": \"${PARTICIPANT_DID}#key-1\", + \"keyGeneratorParams\": { + \"algorithm\": \"EC\" + } + } + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ Consumer participant context created" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Consumer participant context already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + + + + echo "" + echo "================================================" + echo "IdentityHub seeding completed successfully!" + echo "================================================" \ No newline at end of file diff --git a/k8s/consumer/application/identityhub.yaml b/k8s/consumer/application/identityhub.yaml index 8d303d308..a346163bd 100644 --- a/k8s/consumer/application/identityhub.yaml +++ b/k8s/consumer/application/identityhub.yaml @@ -74,6 +74,9 @@ spec: selector: app: identityhub ports: + - port: 7080 + targetPort: 7080 + name: web - port: 7082 targetPort: 7082 name: creds-port diff --git a/k8s/consumer/kustomization.yml b/k8s/consumer/kustomization.yml index 91af9826e..89a29f07d 100644 --- a/k8s/consumer/kustomization.yml +++ b/k8s/consumer/kustomization.yml @@ -20,4 +20,5 @@ resources: - application/controlplane.yaml - application/controlplane-seed.yaml - application/identityhub-config.yaml - - application/identityhub.yaml \ No newline at end of file + - application/identityhub.yaml + - application/identityhub-seed.yaml \ No newline at end of file From ee71d181c57eb17e4c33b9b29c5e254c21d4b1fb Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 13:54:32 +0100 Subject: [PATCH 09/22] add identityhub seeding (provider) --- .../application/identityhub-seed.yaml | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 k8s/provider/application/identityhub-seed.yaml diff --git a/k8s/provider/application/identityhub-seed.yaml b/k8s/provider/application/identityhub-seed.yaml new file mode 100644 index 000000000..5eefe67a4 --- /dev/null +++ b/k8s/provider/application/identityhub-seed.yaml @@ -0,0 +1,206 @@ +# +# Copyright (c) 2026 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 +# + +## This seed job creates a participant context in the consumer's IdentityHub and a holder entry in the IssuerService. + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: identityhub-seed + namespace: provider + labels: + app: identityhub-seed + type: edc-job +spec: + backoffLimit: 5 + template: + metadata: + labels: + app: identityhub-seed + type: edc-job + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-dependencies + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + + command: + - sh + - -c + - | + until curl -sf http://identityhub.provider.svc.cluster.local:7080/api/check/readiness; do + echo "Waiting for identityhub to be ready..." + sleep 5 + done + echo "" + echo "IdentityHub is ready!" + + until curl -sf http://issuerservice.issuer.svc.cluster.local:10010/api/check/readiness; do + echo "Waiting for issuerservice to be ready..." + sleep 5 + done + echo "" + echo "IssuerService is ready!" + + # Wait for Keycloak + echo "Waiting for Keycloak to be ready..." + until wget -q --spider "${KC_HOST}/realms/mvd/.well-known/openid-configuration" > /dev/null 2>&1; do + echo "Keycloak not ready yet, retrying in 2 seconds..." + sleep 2 + done + echo "Keycloak is ready!" + containers: + - name: seed-issuerservice + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + - name: ISSUER_CLIENT_ID + value: "issuer" + - name: ISSUER_CLIENT_SECRET + value: "issuer-secret" + - name: ISSUER_ADMIN_URL + value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" + - name: PARTICIPANT_DID + value: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" + - name: PARTICIPANT_ID + value: "provider-participant" + command: + - sh + - -c + - | + set -e + echo "" + echo "================================================" + echo "Step 1: Create provider Holder in IssuerService" + echo "================================================" + + ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ISSUER_CLIENT_ID}" \ + -d "client_secret=${ISSUER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ISSUER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"did\": \"${PARTICIPANT_DID}\", + \"holderId\": \"${PARTICIPANT_DID}\", + \"name\": \"MVD provider Participant\" + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ provider holder created in IssuerService" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ provider holder already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + - name: seed-identityhub + image: curlimages/curl:latest + env: + - name: KC_HOST + value: "http://keycloak.mvd-common.svc.cluster.local:8080" + - name: PROVISIONER_CLIENT_ID + value: "provisioner" + - name: PROVISIONER_CLIENT_SECRET + value: "provisioner-secret" + - name: IH_URL + value: "http://identityhub.provider.svc.cluster.local:7081/api/identity/v1alpha/participants/" + - name: PARTICIPANT_DID + value: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" + - name: PARTICIPANT_ID + value: "provider-participant" + command: + - sh + - -c + - | + set -e + + echo "================================================" + echo "Step 1: Create provider Participant Context in IdentityHub" + echo "================================================" + + PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${PROVISIONER_CLIENT_ID}" \ + -d "client_secret=${PROVISIONER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$PROVISIONER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${IH_URL}" \ + -H "Authorization: Bearer ${PROVISIONER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"roles\": [], + \"serviceEndpoints\": [ + { + \"type\": \"CredentialService\", + \"serviceEndpoint\": \"http://identityhub.provider.svc.cluster.local:7082/api/credentials/v1/participants/${PARTICIPANT_ID}\", + \"id\": \"provider-credentialservice-1\" + }, + { + \"type\": \"ProtocolEndpoint\", + \"serviceEndpoint\": \"http://controlplane.provider.svc.cluster.local:8082/api/dsp\", + \"id\": \"provider-dsp\" + } + ], + \"active\": true, + \"participantId\": \"${PARTICIPANT_DID}\", + \"participantContextId\": \"${PARTICIPANT_ID}\", + \"did\": \"${PARTICIPANT_DID}\", + \"key\": { + \"keyId\": \"${PARTICIPANT_DID}#key-1\", + \"privateKeyAlias\": \"${PARTICIPANT_DID}#key-1\", + \"keyGeneratorParams\": { + \"algorithm\": \"EC\" + } + } + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ provider participant context created" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ provider participant context already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + + + + echo "" + echo "================================================" + echo "IdentityHub seeding completed successfully!" + echo "================================================" \ No newline at end of file From 07ed4c3262061e76c2c3ff7a26be9d9017703735 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 15:08:08 +0100 Subject: [PATCH 10/22] fix seeding --- k8s/common/gateway.yaml | 31 ++++++++++++++ k8s/common/keycloak.yaml | 42 +++++++++---------- k8s/common/kustomization.yaml | 1 + .../application/controlplane-seed.yaml | 10 ++--- k8s/consumer/application/identityhub.yaml | 2 +- k8s/issuer/base/postgres.yaml | 8 ++-- .../application/controlplane-seed.yaml | 18 ++++---- k8s/provider/application/identityhub.yaml | 2 +- k8s/provider/base/gateway.yaml | 2 +- 9 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 k8s/common/gateway.yaml diff --git a/k8s/common/gateway.yaml b/k8s/common/gateway.yaml new file mode 100644 index 000000000..32158716d --- /dev/null +++ b/k8s/common/gateway.yaml @@ -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 +# + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: common-gateway + namespace: mvd-common +spec: + gatewayClassName: traefik + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All #or Same or Selector +# kinds: +# - kind: HTTPRoute +# group: gateway.networking.k8s.io \ No newline at end of file diff --git a/k8s/common/keycloak.yaml b/k8s/common/keycloak.yaml index 216f8487d..5cf2d1c8b 100644 --- a/k8s/common/keycloak.yaml +++ b/k8s/common/keycloak.yaml @@ -268,24 +268,24 @@ spec: targetPort: 9000 name: health -#--- -#apiVersion: gateway.networking.k8s.io/v1 -#kind: HTTPRoute -#metadata: -# name: keycloak -# namespace: mvd-common -#spec: -# parentRefs: -# - name: issuer-gateway -# kind: Gateway -# sectionName: http -# hostnames: -# - keycloak.issuer.localhost -# rules: -# - matches: -# - path: -# type: PathPrefix -# value: / -# backendRefs: -# - name: keycloak -# port: 8080 \ No newline at end of file +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: keycloak + namespace: mvd-common +spec: + parentRefs: + - name: common-gateway + kind: Gateway + sectionName: http + hostnames: + - keycloak.localhost + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: keycloak + port: 8080 \ No newline at end of file diff --git a/k8s/common/kustomization.yaml b/k8s/common/kustomization.yaml index 0056c0646..e7f8ace2b 100644 --- a/k8s/common/kustomization.yaml +++ b/k8s/common/kustomization.yaml @@ -1,5 +1,6 @@ resources: - namespace.yaml - gateway-class.yaml + - gateway.yaml - postgres.yaml - keycloak.yaml \ No newline at end of file diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml index 74a3c9814..65ba47edc 100644 --- a/k8s/consumer/application/controlplane-seed.yaml +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -79,7 +79,7 @@ spec: echo "" echo "================================================" - echo "Step 2: Create Dataproccesor CEL Expression" + echo "Step 2: Create ManufacturerCredential CEL Expression" echo "================================================" create_cel '{ @@ -88,17 +88,17 @@ spec: ], "@type": "CelExpression", "@id": "membership-cel", - "leftOperand": "DataProcessorCredential", - "description": "Expression for evaluating data processor credential", + "leftOperand": "ManufacturerCredential", + "description": "Expression for evaluating manufacturer credential", "scopes": [ "catalog", "contract.negotiation", "transfer.process" ], - "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''DataProcessorCredential'\'')).size() > 0" + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''ManufacturerCredential'\'')).size() > 0" }' - echo "✓ Dataprocessor CEL Expression done" + echo "✓ ManufacturerCredential CEL Expression done" echo "" echo "================================================" diff --git a/k8s/consumer/application/identityhub.yaml b/k8s/consumer/application/identityhub.yaml index a346163bd..e65c0e531 100644 --- a/k8s/consumer/application/identityhub.yaml +++ b/k8s/consumer/application/identityhub.yaml @@ -101,7 +101,7 @@ metadata: namespace: consumer spec: parentRefs: - - name: edcv-gateway + - name: consumer-gateway kind: Gateway sectionName: http hostnames: diff --git a/k8s/issuer/base/postgres.yaml b/k8s/issuer/base/postgres.yaml index 9fd6e5fc5..626f39136 100644 --- a/k8s/issuer/base/postgres.yaml +++ b/k8s/issuer/base/postgres.yaml @@ -37,11 +37,11 @@ spec: - containerPort: 5432 env: - name: POSTGRES_DB - value: "controlplane" + value: "issuerservice" - name: POSTGRES_USER - value: "cp" + value: "issuer" - name: POSTGRES_PASSWORD - value: "cp" + value: "issuer" volumeMounts: - name: init-script mountPath: /docker-entrypoint-initdb.d @@ -70,8 +70,6 @@ metadata: namespace: issuer data: init.sql: | - CREATE DATABASE issuerservice; - CREATE USER issuer WITH PASSWORD 'issuer'; GRANT ALL PRIVILEGES ON DATABASE issuerservice TO issuer; \c issuerservice GRANT ALL ON SCHEMA public TO issuer; diff --git a/k8s/provider/application/controlplane-seed.yaml b/k8s/provider/application/controlplane-seed.yaml index a4be78413..068ba33e4 100644 --- a/k8s/provider/application/controlplane-seed.yaml +++ b/k8s/provider/application/controlplane-seed.yaml @@ -212,7 +212,7 @@ spec: echo "" echo "================================================" - echo "Step 2: Create DataProcessor Policy" + echo "Step 2: Create Manufacturer Policy" echo "================================================" create_policy '{ @@ -220,23 +220,23 @@ spec: "https://w3id.org/edc/connector/management/v2" ], "@type": "PolicyDefinition", - "@id": "require-dataprocessor", + "@id": "require-manufacturer", "policy": { "@type": "Set", "obligation": [ { "action": "use", "constraint": { - "leftOperand": "DataAccess.level", + "leftOperand": "ManufacturerCredential", "operator": "eq", - "rightOperand": "processing" + "rightOperand": "active" } } ] } }' - echo "✓ DataProcessor policy done" + echo "✓ Manufacturer policy done" echo "" echo "================================================" @@ -332,17 +332,17 @@ spec: } echo "================================================" - echo "Step 1: Create member-and-dataprocessor-def" + echo "Step 1: Create member-and-manufacturer-def" echo "================================================" create_contractdef '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], - "@id": "member-and-dataprocessor-def", + "@id": "member-and-manufacturer-def", "@type": "ContractDefinition", "accessPolicyId": "require-membership", - "contractPolicyId": "require-dataprocessor", + "contractPolicyId": "require-manufacturer", "assetsSelector": { "@type": "Criterion", "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", @@ -351,7 +351,7 @@ spec: } }' - echo "✓ member-and-dataprocessor-def done" + echo "✓ member-and-manufacturer-def done" echo "" echo "================================================" diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml index 5b1f42378..b5b66627e 100644 --- a/k8s/provider/application/identityhub.yaml +++ b/k8s/provider/application/identityhub.yaml @@ -98,7 +98,7 @@ metadata: namespace: provider spec: parentRefs: - - name: edcv-gateway + - name: provider-gateway kind: Gateway sectionName: http hostnames: diff --git a/k8s/provider/base/gateway.yaml b/k8s/provider/base/gateway.yaml index 5454bda5e..154e155e9 100644 --- a/k8s/provider/base/gateway.yaml +++ b/k8s/provider/base/gateway.yaml @@ -15,7 +15,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: consumer-gateway + name: provider-gateway namespace: provider spec: gatewayClassName: traefik From b775cc2a86d7deb1dd053a2251452446856f767e Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 15:42:37 +0100 Subject: [PATCH 11/22] add credential issuance --- .../application/identityhub-seed.yaml | 174 ++++++++++++------ 1 file changed, 118 insertions(+), 56 deletions(-) diff --git a/k8s/consumer/application/identityhub-seed.yaml b/k8s/consumer/application/identityhub-seed.yaml index 56b91bcda..6739f0629 100644 --- a/k8s/consumer/application/identityhub-seed.yaml +++ b/k8s/consumer/application/identityhub-seed.yaml @@ -64,62 +64,7 @@ spec: done echo "Keycloak is ready!" containers: - - name: seed-issuerservice - image: curlimages/curl:latest - env: - - name: KC_HOST - value: "http://keycloak.mvd-common.svc.cluster.local:8080" - - name: ISSUER_CLIENT_ID - value: "issuer" - - name: ISSUER_CLIENT_SECRET - value: "issuer-secret" - - name: ISSUER_ADMIN_URL - value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" - - name: PARTICIPANT_DID - value: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" - - name: PARTICIPANT_ID - value: "consumer-participant" - command: - - sh - - -c - - | - set -e - echo "" - echo "================================================" - echo "Step 1: Create Consumer Holder in IssuerService" - echo "================================================" - ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=client_credentials" \ - -d "client_id=${ISSUER_CLIENT_ID}" \ - -d "client_secret=${ISSUER_CLIENT_SECRET}" \ - -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') - - if [ -z "$ISSUER_TOKEN" ]; then - echo "Failed to get issuer token" - exit 1 - fi - - RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ - -H "Authorization: Bearer ${ISSUER_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"did\": \"${PARTICIPANT_DID}\", - \"holderId\": \"${PARTICIPANT_DID}\", - \"name\": \"MVD Consumer Participant\" - }") - HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) - BODY=$(echo "${RESPONSE}" | sed '$d') - if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then - echo "✓ Consumer holder created in IssuerService" - elif [ "${HTTP_STATUS}" -eq 409 ]; then - echo "⚠ Consumer holder already exists (HTTP 409), skipping." - else - echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" - echo "Response body: ${BODY}" - exit 1 - fi - name: seed-identityhub image: curlimages/curl:latest env: @@ -129,20 +74,67 @@ spec: value: "provisioner" - name: PROVISIONER_CLIENT_SECRET value: "provisioner-secret" + - name: ADMIN_CLIENT_ID + value: "admin" + - name: ADMIN_CLIENT_SECRET + value: "edc-v-admin-secret" - name: IH_URL value: "http://identityhub.consumer.svc.cluster.local:7081/api/identity/v1alpha/participants/" - name: PARTICIPANT_DID value: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" - name: PARTICIPANT_ID value: "consumer-participant" + - name: ISSUER_CLIENT_ID + value: "issuer" + - name: ISSUER_CLIENT_SECRET + value: "issuer-secret" + - name: ISSUER_ADMIN_URL + value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" command: - sh - -c - | set -e + + echo "" + echo "================================================" + echo "Step 1: Create Consumer Holder in IssuerService" + echo "================================================" + + ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ISSUER_CLIENT_ID}" \ + -d "client_secret=${ISSUER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ISSUER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"did\": \"${PARTICIPANT_DID}\", + \"holderId\": \"${PARTICIPANT_DID}\", + \"name\": \"MVD Consumer Participant\" + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ Consumer holder created in IssuerService" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ Consumer holder already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi echo "================================================" - echo "Step 1: Create Consumer Participant Context in IdentityHub" + echo "Step 2: Create Consumer Participant Context in IdentityHub" echo "================================================" PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ @@ -198,7 +190,77 @@ spec: exit 1 fi + echo "================================================" + echo "Step 3: Requesting credential issuance" + echo "================================================" + ADMIN_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ADMIN_CLIENT_ID}" \ + -d "client_secret=${ADMIN_CLIENT_SECRET}" \ + -d "scope=identity-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ADMIN_TOKEN" ]; then + echo "Failed to get OAuth2 token" + exit 1 + fi + + HOLDER_PID="credential-request-1" + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${IH_URL}${PARTICIPANT_ID}/credentials/request" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"issuerDid\": \"did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer\", + \"holderPid\": \"${HOLDER_PID}\", + \"credentials\": [{ + \"format\": \"VC1_0_JWT\", + \"type\": \"MembershipCredential\", + \"id\": \"membership-credential-def\" + }, + { + \"format\": \"VC1_0_JWT\", + \"type\": \"ManufacturerCredential\", + \"id\": \"manufacturer-credential-def\" + }] + }") + + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + if [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ Consumer participant context created" + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${RESPONSE}" + exit 1 + fi + + echo "================================================" + echo "Step 4: Wait for credentials to be issued" + echo "================================================" + + STATUS="" + MAX_RETRIES=30 + RETRY=0 + while [ "${STATUS}" != "ISSUED" ]; do + if [ "${RETRY}" -ge "${MAX_RETRIES}" ]; then + echo "✗ Credentials not ISSUED after ${MAX_RETRIES} attempts" + exit 1 + fi + RESPONSE=$(curl -sS -w "\n%{http_code}" "${IH_URL}${PARTICIPANT_ID}/credentials/request/${HOLDER_PID}" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -ne 200 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + STATUS=$(echo "${BODY}" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') + echo "Credential status: ${STATUS}" + RETRY=$((RETRY + 1)) + sleep 5 + done + echo "✓ Credentials are ISSUED" echo "" echo "================================================" From 1ca68af5bcea32ebf2ab015f412eb81f5e1d6d12 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 26 Mar 2026 16:21:11 +0100 Subject: [PATCH 12/22] provider credential request --- .../application/identityhub-seed.yaml | 4 +- k8s/provider/application/controlplane.yaml | 2 +- .../application/identityhub-config.yaml | 4 +- .../application/identityhub-seed.yaml | 178 ++++++++++++------ k8s/provider/application/identityhub.yaml | 3 + k8s/provider/base/vault.yaml | 2 +- k8s/provider/kustomization.yml | 3 +- 7 files changed, 131 insertions(+), 65 deletions(-) diff --git a/k8s/consumer/application/identityhub-seed.yaml b/k8s/consumer/application/identityhub-seed.yaml index 6739f0629..6caf3d9a1 100644 --- a/k8s/consumer/application/identityhub-seed.yaml +++ b/k8s/consumer/application/identityhub-seed.yaml @@ -11,7 +11,7 @@ # Metaform Systems, Inc. - initial API and implementation # -## This seed job creates a participant context in the consumer's IdentityHub and a holder entry in the IssuerService. +# This seed job creates a participant context in the consumer's IdentityHub and a holder entry in the IssuerService. --- apiVersion: batch/v1 @@ -50,7 +50,7 @@ spec: echo "IdentityHub is ready!" until curl -sf http://issuerservice.issuer.svc.cluster.local:10010/api/check/readiness; do - echo "Waiting for issuerservice to be ready..." + echo "Waiting for issuer service to be ready..." sleep 5 done echo "" diff --git a/k8s/provider/application/controlplane.yaml b/k8s/provider/application/controlplane.yaml index 4742c07dd..90628a48d 100644 --- a/k8s/provider/application/controlplane.yaml +++ b/k8s/provider/application/controlplane.yaml @@ -96,7 +96,7 @@ metadata: namespace: provider spec: parentRefs: - - name: consumer-gateway + - name: provider-gateway kind: Gateway sectionName: http hostnames: diff --git a/k8s/provider/application/identityhub-config.yaml b/k8s/provider/application/identityhub-config.yaml index 124705f80..16fde7310 100644 --- a/k8s/provider/application/identityhub-config.yaml +++ b/k8s/provider/application/identityhub-config.yaml @@ -51,5 +51,5 @@ data: # to do this properly, we should probably configure the following properties on the ingress route: # proxy_set_header Host $host; # proxy_set_header X-Forwarded-Proto $scheme; - edc.iam.oauth2.issuer: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv" - edc.iam.oauth2.jwks.url: "http://keycloak.provider.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/certs" \ No newline at end of file + edc.iam.oauth2.issuer: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd" + edc.iam.oauth2.jwks.url: "http://keycloak.mvd-common.svc.cluster.local:8080/realms/mvd/protocol/openid-connect/certs" \ No newline at end of file diff --git a/k8s/provider/application/identityhub-seed.yaml b/k8s/provider/application/identityhub-seed.yaml index 5eefe67a4..0c9a43826 100644 --- a/k8s/provider/application/identityhub-seed.yaml +++ b/k8s/provider/application/identityhub-seed.yaml @@ -11,7 +11,7 @@ # Metaform Systems, Inc. - initial API and implementation # -## This seed job creates a participant context in the consumer's IdentityHub and a holder entry in the IssuerService. +# This seed job creates a participant context in the provider's IdentityHub and a holder entry in the IssuerService. --- apiVersion: batch/v1 @@ -50,7 +50,7 @@ spec: echo "IdentityHub is ready!" until curl -sf http://issuerservice.issuer.svc.cluster.local:10010/api/check/readiness; do - echo "Waiting for issuerservice to be ready..." + echo "Waiting for issuer service to be ready..." sleep 5 done echo "" @@ -64,62 +64,7 @@ spec: done echo "Keycloak is ready!" containers: - - name: seed-issuerservice - image: curlimages/curl:latest - env: - - name: KC_HOST - value: "http://keycloak.mvd-common.svc.cluster.local:8080" - - name: ISSUER_CLIENT_ID - value: "issuer" - - name: ISSUER_CLIENT_SECRET - value: "issuer-secret" - - name: ISSUER_ADMIN_URL - value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" - - name: PARTICIPANT_DID - value: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" - - name: PARTICIPANT_ID - value: "provider-participant" - command: - - sh - - -c - - | - set -e - echo "" - echo "================================================" - echo "Step 1: Create provider Holder in IssuerService" - echo "================================================" - ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=client_credentials" \ - -d "client_id=${ISSUER_CLIENT_ID}" \ - -d "client_secret=${ISSUER_CLIENT_SECRET}" \ - -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') - - if [ -z "$ISSUER_TOKEN" ]; then - echo "Failed to get issuer token" - exit 1 - fi - - RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ - -H "Authorization: Bearer ${ISSUER_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"did\": \"${PARTICIPANT_DID}\", - \"holderId\": \"${PARTICIPANT_DID}\", - \"name\": \"MVD provider Participant\" - }") - HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) - BODY=$(echo "${RESPONSE}" | sed '$d') - if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then - echo "✓ provider holder created in IssuerService" - elif [ "${HTTP_STATUS}" -eq 409 ]; then - echo "⚠ provider holder already exists (HTTP 409), skipping." - else - echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" - echo "Response body: ${BODY}" - exit 1 - fi - name: seed-identityhub image: curlimages/curl:latest env: @@ -129,20 +74,67 @@ spec: value: "provisioner" - name: PROVISIONER_CLIENT_SECRET value: "provisioner-secret" + - name: ADMIN_CLIENT_ID + value: "admin" + - name: ADMIN_CLIENT_SECRET + value: "edc-v-admin-secret" - name: IH_URL value: "http://identityhub.provider.svc.cluster.local:7081/api/identity/v1alpha/participants/" - name: PARTICIPANT_DID value: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" - name: PARTICIPANT_ID value: "provider-participant" + - name: ISSUER_CLIENT_ID + value: "issuer" + - name: ISSUER_CLIENT_SECRET + value: "issuer-secret" + - name: ISSUER_ADMIN_URL + value: "http://issuerservice.issuer.svc.cluster.local:10013/api/admin/v1alpha" command: - sh - -c - | set -e + + echo "" + echo "================================================" + echo "Step 1: Create provider Holder in IssuerService" + echo "================================================" + + ISSUER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ISSUER_CLIENT_ID}" \ + -d "client_secret=${ISSUER_CLIENT_SECRET}" \ + -d "scope=issuer-admin-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ISSUER_TOKEN" ]; then + echo "Failed to get issuer token" + exit 1 + fi + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${ISSUER_ADMIN_URL}/participants/issuer/holders" \ + -H "Authorization: Bearer ${ISSUER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"did\": \"${PARTICIPANT_DID}\", + \"holderId\": \"${PARTICIPANT_DID}\", + \"name\": \"MVD provider Participant\" + }") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -eq 200 ] || [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ provider holder created in IssuerService" + elif [ "${HTTP_STATUS}" -eq 409 ]; then + echo "⚠ provider holder already exists (HTTP 409), skipping." + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi echo "================================================" - echo "Step 1: Create provider Participant Context in IdentityHub" + echo "Step 2: Create provider Participant Context in IdentityHub" echo "================================================" PROVISIONER_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ @@ -198,7 +190,77 @@ spec: exit 1 fi + echo "================================================" + echo "Step 3: Requesting credential issuance" + echo "================================================" + ADMIN_TOKEN=$(curl -sf -X POST "${KC_HOST}/realms/mvd/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ADMIN_CLIENT_ID}" \ + -d "client_secret=${ADMIN_CLIENT_SECRET}" \ + -d "scope=identity-api:write" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + + if [ -z "$ADMIN_TOKEN" ]; then + echo "Failed to get OAuth2 token" + exit 1 + fi + + HOLDER_PID="credential-request-1" + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${IH_URL}${PARTICIPANT_ID}/credentials/request" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"issuerDid\": \"did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer\", + \"holderPid\": \"${HOLDER_PID}\", + \"credentials\": [{ + \"format\": \"VC1_0_JWT\", + \"type\": \"MembershipCredential\", + \"id\": \"membership-credential-def\" + }, + { + \"format\": \"VC1_0_JWT\", + \"type\": \"ManufacturerCredential\", + \"id\": \"manufacturer-credential-def\" + }] + }") + + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + if [ "${HTTP_STATUS}" -eq 201 ]; then + echo "✓ provider participant context created" + else + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${RESPONSE}" + exit 1 + fi + + echo "================================================" + echo "Step 4: Wait for credentials to be issued" + echo "================================================" + + STATUS="" + MAX_RETRIES=30 + RETRY=0 + while [ "${STATUS}" != "ISSUED" ]; do + if [ "${RETRY}" -ge "${MAX_RETRIES}" ]; then + echo "✗ Credentials not ISSUED after ${MAX_RETRIES} attempts" + exit 1 + fi + RESPONSE=$(curl -sS -w "\n%{http_code}" "${IH_URL}${PARTICIPANT_ID}/credentials/request/${HOLDER_PID}" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') + if [ "${HTTP_STATUS}" -ne 200 ]; then + echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" + exit 1 + fi + STATUS=$(echo "${BODY}" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') + echo "Credential status: ${STATUS}" + RETRY=$((RETRY + 1)) + sleep 5 + done + echo "✓ Credentials are ISSUED" echo "" echo "================================================" diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml index b5b66627e..376b72f53 100644 --- a/k8s/provider/application/identityhub.yaml +++ b/k8s/provider/application/identityhub.yaml @@ -74,6 +74,9 @@ spec: selector: app: identityhub ports: + - port: 7080 + targetPort: 7080 + name: web - port: 7082 targetPort: 7082 name: creds-port diff --git a/k8s/provider/base/vault.yaml b/k8s/provider/base/vault.yaml index b273ee4e9..e8b2b5e2e 100644 --- a/k8s/provider/base/vault.yaml +++ b/k8s/provider/base/vault.yaml @@ -198,7 +198,7 @@ metadata: namespace: provider spec: parentRefs: - - name: consumer-gateway + - name: provider-gateway hostnames: - vault.provider.localhost rules: diff --git a/k8s/provider/kustomization.yml b/k8s/provider/kustomization.yml index 91af9826e..89a29f07d 100644 --- a/k8s/provider/kustomization.yml +++ b/k8s/provider/kustomization.yml @@ -20,4 +20,5 @@ resources: - application/controlplane.yaml - application/controlplane-seed.yaml - application/identityhub-config.yaml - - application/identityhub.yaml \ No newline at end of file + - application/identityhub.yaml + - application/identityhub-seed.yaml \ No newline at end of file From c6d5f638975ddea1d17c0d0c162a3769fa93e4d0 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 08:15:19 +0100 Subject: [PATCH 13/22] e2e test pt1 --- README.md | 6 +- .../application/controlplane-config.yaml | 12 +- .../application/controlplane-config.yaml | 11 +- .../application/controlplane-seed.yaml | 4 +- k8s/provider/application/identityhub.yaml | 3 - .../demo/tests/transfer/CatalogResponse.java | 134 ++++++++++++++++++ .../tests/transfer/TransferEndToEndTest.java | 101 +++++++------ .../test/resources/negotiation-request.json | 10 +- .../src/test/resources/transfer-request.json | 5 +- 9 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java diff --git a/README.md b/README.md index b0d71a4df..efa4cacb3 100644 --- a/README.md +++ b/README.md @@ -741,7 +741,7 @@ a token in the Authorization header, which authorizes the verifier to obtain the consumer's IdentityHub. We achieve this by intercepting the DSP request and adding the correct scope - here: -`"org.eclipse.edc.vc.type:MembershipCredential:read"` - to the request builder. Technically, this is achieved by +`"org.eclipse.dspace.dcp.vc.type:MembershipCredential:read"` - to the request builder. Technically, this is achieved by registering a `postValidator` function for the relevant policy scopes, check out the [DcpPatchExtension.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java) class. @@ -769,7 +769,7 @@ relevant scope string to the access token upon DSP egress. A policy, that requir The [DataAccessCredentialScopeExtractor.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java) -class would convert this into a scope string `org.eclipse.edc.vc.type:DataProcessorCredential:read` and add it to the +class would convert this into a scope string `org.eclipse.dspace.dcp.vc.type:DataProcessorCredential:read` and add it to the consumer's access token. ### 8.4 Policy evaluation functions @@ -934,7 +934,7 @@ deliver a credential of type `DemoCredential` to the consumer's IdentityHub. ### 10.4 Default scope-to-criterion transformer When IdentityHub receives a Presentation query, that carries an access token, it must be able to convert a scope string -into a filter expression, for example `org.eclipse.edc.vc.type:DataProcessorCredential:read` is converted into +into a filter expression, for example `org.eclipse.dspace.dcp.vc.type:DataProcessorCredential:read` is converted into `verifiableCredential.credential.type = DataProcessorCredential`. This filter expression is then used by IdentityHub to query for `DataProcessorCredentials` in the database. diff --git a/k8s/consumer/application/controlplane-config.yaml b/k8s/consumer/application/controlplane-config.yaml index 6827fa974..28b50fd90 100644 --- a/k8s/consumer/application/controlplane-config.yaml +++ b/k8s/consumer/application/controlplane-config.yaml @@ -22,9 +22,13 @@ data: edc.hostname: "controlplane.consumer.svc.cluster.local" edc.vault.hashicorp.url: "http://vault.consumer.svc.cluster.local:8200" edc.vault.hashicorp.token: "root" - edc.participant.id: "did:web:connector" + edc.participant.id: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" edc.iam.did.web.use.https: "false" edc.iam.credential.revocation.mimetype: "application/json" + edc.iam.sts.oauth.token.url: "http://identityhub.consumer.svc.cluster.local:7084/api/sts/token" + edc.iam.sts.oauth.client.id: "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer" + edc.iam.sts.oauth.client.secret.alias: "consumer-participant-sts-client-secret" + # web config web.http.port: "8080" @@ -55,14 +59,14 @@ data: # Default scopes config edc.iam.dcp.scopes.membership.id: "membership-scope" edc.iam.dcp.scopes.membership.type: "DEFAULT" - edc.iam.dcp.scopes.membership.value: "org.eclipse.edc.vc.type:MembershipCredential:read" + edc.iam.dcp.scopes.membership.value: "org.eclipse.dspace.dcp.vc.type:MembershipCredential:read" edc.iam.dcp.scopes.manufacturer.id: "manufacturer-scope" edc.iam.dcp.scopes.manufacturer.type: "POLICY" - edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.edc.vc.type:ManufacturerCredential:read" + edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.dspace.dcp.vc.type:ManufacturerCredential:read" edc.iam.dcp.scopes.manufacturer.prefix-mapping: "ManufacturerCredential" # Trusted Issuers - edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.consumer.svc.cluster.local%3A10016:issuer" + edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" \ No newline at end of file diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml index 21647e771..11d6f8be6 100644 --- a/k8s/provider/application/controlplane-config.yaml +++ b/k8s/provider/application/controlplane-config.yaml @@ -22,11 +22,14 @@ data: edc.hostname: "controlplane.provider.svc.cluster.local" edc.vault.hashicorp.url: "http://vault.provider.svc.cluster.local:8200" edc.vault.hashicorp.token: "root" - edc.participant.id: "did:web:connector" + edc.participant.id: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" edc.iam.did.web.use.https: "false" edc.iam.credential.revocation.mimetype: "application/json" # todo: should this be true? edc.policy.validation.enabled: "false" + edc.iam.sts.oauth.token.url: "http://identityhub.provider.svc.cluster.local:7084/api/sts/token" + edc.iam.sts.oauth.client.id: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" + edc.iam.sts.oauth.client.secret.alias: "provider-participant-sts-client-secret" # web config web.http.port: "8080" @@ -57,14 +60,14 @@ data: # Default scopes config edc.iam.dcp.scopes.membership.id: "membership-scope" edc.iam.dcp.scopes.membership.type: "DEFAULT" - edc.iam.dcp.scopes.membership.value: "org.eclipse.edc.vc.type:MembershipCredential:read" + edc.iam.dcp.scopes.membership.value: "org.eclipse.dspace.dcp.vc.type:MembershipCredential:read" edc.iam.dcp.scopes.manufacturer.id: "manufacturer-scope" edc.iam.dcp.scopes.manufacturer.type: "POLICY" - edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.edc.vc.type:ManufacturerCredential:read" + edc.iam.dcp.scopes.manufacturer.value: "org.eclipse.dspace.dcp.vc.type:ManufacturerCredential:read" edc.iam.dcp.scopes.manufacturer.prefix-mapping: "ManufacturerCredential" # Trusted Issuers - edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.provider.svc.cluster.local%3A10016:issuer" + edc.iam.trusted-issuer.issuer.id: "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" \ No newline at end of file diff --git a/k8s/provider/application/controlplane-seed.yaml b/k8s/provider/application/controlplane-seed.yaml index 068ba33e4..11073eedd 100644 --- a/k8s/provider/application/controlplane-seed.yaml +++ b/k8s/provider/application/controlplane-seed.yaml @@ -257,9 +257,9 @@ spec: { "action": "use", "constraint": { - "leftOperand": "DataAccess.level", + "leftOperand": "ManufacturerCredential", "operator": "eq", - "rightOperand": "sensitive" + "rightOperand": "active" } } ] diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml index 376b72f53..b5b66627e 100644 --- a/k8s/provider/application/identityhub.yaml +++ b/k8s/provider/application/identityhub.yaml @@ -74,9 +74,6 @@ spec: selector: app: identityhub ports: - - port: 7080 - targetPort: 7080 - name: web - port: 7082 targetPort: 7082 name: creds-port diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java new file mode 100644 index 000000000..2750108f8 --- /dev/null +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.demo.tests.transfer; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CatalogResponse { + + @JsonProperty("@id") + private String id; + + @JsonProperty("participantId") + private String participantId; + + @JsonProperty("dataset") + private List datasets; + + @JsonProperty("service") + private List services; + + public String getId() { return id; } + + public String getParticipantId() { return participantId; } + + public List getDatasets() { return datasets; } + + public List getServices() { return services; } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Dataset { + + @JsonProperty("@id") + private String id; + + @JsonProperty("description") + private String description; + + @JsonProperty("hasPolicy") + private List policies; + + @JsonProperty("distribution") + private List distributions; + + public String getId() { return id; } + + public String getDescription() { return description; } + + public List getPolicies() { return policies; } + + public List getDistributions() { return distributions; } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Offer { + + @JsonProperty("@id") + private String id; + + @JsonProperty("obligation") + private List obligations; + + public String getId() { return id; } + + public List getObligations() { return obligations; } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Obligation { + + @JsonProperty("action") + private String action; + + @JsonProperty("constraint") + private List constraints; + + public String getAction() { return action; } + + public List getConstraints() { return constraints; } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Constraint { + + @JsonProperty("leftOperand") + private String leftOperand; + + @JsonProperty("operator") + private String operator; + + @JsonProperty("rightOperand") + private String rightOperand; + + public String getLeftOperand() { return leftOperand; } + + public String getOperator() { return operator; } + + public String getRightOperand() { return rightOperand; } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class DataService { + + @JsonProperty("@id") + private String id; + + @JsonProperty("endpointDescription") + private String endpointDescription; + + @JsonProperty("endpointURL") + private String endpointUrl; + + public String getId() { return id; } + + public String getEndpointDescription() { return endpointDescription; } + + public String getEndpointUrl() { return endpointUrl; } + } +} \ No newline at end of file diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java index 1cf52b6c8..a970c8ef1 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java @@ -14,9 +14,11 @@ package org.eclipse.edc.demo.tests.transfer; +import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.specification.RequestSpecification; import jakarta.json.Json; import jakarta.json.JsonArray; +import jakarta.json.JsonObject; import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; import org.eclipse.edc.connector.controlplane.transform.odrl.OdrlTransformersFactory; @@ -27,6 +29,7 @@ import org.eclipse.edc.junit.testfixtures.TestUtils; import org.eclipse.edc.participant.spi.ParticipantIdMapper; import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; @@ -51,19 +54,20 @@ @EndToEndTest public class TransferEndToEndTest { // Management API base URL of the consumer connector, goes through Ingress controller - private static final String CONSUMER_MANAGEMENT_URL = "http://127.0.0.1/consumer/cp"; + private static final String CONSUMER_MANAGEMENT_URL = "http://cp.consumer.localhost:8080"; // Catalog Query API URL of the consumer connector, goes through ingress controller private static final String CONSUMER_CATALOG_URL = "http://127.0.0.1/consumer/fc"; // DSP service URL of the provider, not reachable outside the cluster - private static final String PROVIDER_DSP_URL = "http://provider-qna-controlplane:8082"; + private static final String PROVIDER_DSP_URL = "http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1"; // DID of the provider company - private static final String PROVIDER_ID = "did:web:provider-identityhub%3A7083:provider"; + private static final String PROVIDER_ID = "did:web:identityhub.provider.svc.cluster.local%3A7083:provider"; // public API endpoint of the provider-qna connector, goes through the ingress controller private static final String PROVIDER_PUBLIC_URL = "http://127.0.0.1/provider-qna/public"; - private static final String PROVIDER_MANAGEMENT_URL = "http://127.0.0.1/provider-qna/cp"; + private static final String PROVIDER_MANAGEMENT_URL = "http://cp.provider.localhost:8080"; private final TypeTransformerRegistry transformerRegistry = new TypeTransformerRegistryImpl(); private final JsonLd jsonLd = new TitaniumJsonLd(new ConsoleMonitor()); + private final ObjectMapper objectMapper = new ObjectMapper(); private static RequestSpecification baseRequest() { return given() @@ -98,56 +102,49 @@ public String fromIri(String s) { void transferData_hasPermission_shouldTransferData() { System.out.println("Waiting for Provider dataplane to come online"); // wait until provider's dataplane is available - await().atMost(TEST_TIMEOUT_DURATION) - .pollDelay(TEST_POLL_DELAY) - .untilAsserted(() -> { - var jp = baseRequest() - .get(PROVIDER_MANAGEMENT_URL + "/api/management/v3/dataplanes") - .then() - .statusCode(200) - .log().ifValidationFails() - .extract().body().jsonPath(); - - var state = jp.getString("state"); - assertThat(state).isEqualTo("[AVAILABLE]"); - }); - - System.out.println("Provider dataplane is online, fetching catalog"); - - var emptyQueryBody = Json.createObjectBuilder() - .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/v0.0.1/ns/")) - .add("@type", "QuerySpec") +// await().atMost(TEST_TIMEOUT_DURATION) +// .pollDelay(TEST_POLL_DELAY) +// .untilAsserted(() -> { +// var jp = baseRequest() +// .get(PROVIDER_MANAGEMENT_URL + "/api/mgmt/v4beta/dataplanes") +// .then() +// .statusCode(200) +// .log().ifValidationFails() +// .extract().body().jsonPath(); +// +// var state = jp.getString("state"); +// assertThat(state).isEqualTo("[AVAILABLE]"); +// }); +// +// System.out.println("Provider dataplane is online, fetching catalog"); + + var queryBody = Json.createObjectBuilder() + .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/connector/management/v2")) + .add("@type", "CatalogRequest") + .add("counterPartyId", PROVIDER_ID) + .add("counterPartyAddress", "http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1") + .add("protocol", "dataspace-protocol-http:2025-1") + .add("querySpec", Json.createObjectBuilder().build()) .build(); var offerId = new AtomicReference(); // get catalog, extract offer ID await().atMost(TEST_TIMEOUT_DURATION) .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { - var jo = baseRequest() - .body(emptyQueryBody) - .post(CONSUMER_CATALOG_URL + "/api/catalog/v1alpha/catalog/query") + var res = baseRequest() + .body(queryBody) + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/catalog/request") .then() - .log().ifError() + .log().ifValidationFails() .statusCode(200) - .extract().body().as(JsonArray.class); - - var offerIdsFiltered = jo.stream().map(jv -> { - - var expanded = jsonLd.expand(jv.asJsonObject()).orElseThrow(f -> new AssertionError(f.getFailureDetail())); - var cat = transformerRegistry.transform(expanded, Catalog.class).orElseThrow(f -> new AssertionError(f.getFailureDetail())); - return cat.getDatasets().stream().filter(ds -> ds instanceof Catalog) // filter for CatalogAssets - .map(ds -> (Catalog) ds) - .filter(sc -> sc.getDataServices().stream().anyMatch(dataService -> dataService.getEndpointUrl().contains("provider-qna"))) // filter for assets from the Q&A Provider - .flatMap(c -> c.getDatasets().stream()) - .filter(dataset -> dataset.getId().equals("asset-1")) // filter for the asset we're allowed to negotiate - .map(Dataset::getOffers) - .map(offers -> offers.keySet().iterator().next()) - .findFirst() - .orElse(null); - }).toList(); - assertThat(offerIdsFiltered).hasSize(1).doesNotContainNull(); - var oid = offerIdsFiltered.get(0); - assertThat(oid).isNotNull(); + .extract().body().as(JsonObject.class); + + // todo: parse asset offer ID, parse JSON + var cat = objectMapper.readValue(res.toString(), CatalogResponse.class); + var oid = cat.getDatasets().stream().filter(ds -> ds.getId().equals("asset-2")) + .flatMap(ds -> ds.getPolicies().stream()) + .map(CatalogResponse.Offer::getId) + .findFirst().orElseThrow(() -> new AssertionError("No offer found for asset-2")); offerId.set(oid); }); @@ -160,7 +157,7 @@ void transferData_hasPermission_shouldTransferData() { .replace("{{OFFER_ID}}", offerId.get()); var negotiationId = baseRequest() .body(negotiationRequest) - .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations") + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/contractnegotiations") .then() .log().ifError() .statusCode(200) @@ -174,7 +171,7 @@ void transferData_hasPermission_shouldTransferData() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .get(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations/" + negotiationId) + .get(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/contractnegotiations/" + negotiationId) .then() .statusCode(200) .extract().body().jsonPath(); @@ -192,7 +189,7 @@ void transferData_hasPermission_shouldTransferData() { var transferProcessId = baseRequest() .body(tpRequest) - .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/transferprocesses") + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/transferprocesses") .then() .log().ifError() .statusCode(200) @@ -204,8 +201,8 @@ void transferData_hasPermission_shouldTransferData() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .body(emptyQueryBody) - .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/transferprocesses/request") + .body(queryBody) + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/transferprocesses/request") .then() .statusCode(200) .extract().body().jsonPath(); @@ -221,7 +218,7 @@ void transferData_hasPermission_shouldTransferData() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .get(CONSUMER_MANAGEMENT_URL + "/api/management/v3/edrs/%s/dataaddress".formatted(transferProcessId)) + .get(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/edrs/%s/dataaddress".formatted(transferProcessId)) .then() .log().ifValidationFails() .statusCode(200) diff --git a/tests/end2end/src/test/resources/negotiation-request.json b/tests/end2end/src/test/resources/negotiation-request.json index 15358240a..7e11ef2d0 100644 --- a/tests/end2end/src/test/resources/negotiation-request.json +++ b/tests/end2end/src/test/resources/negotiation-request.json @@ -3,9 +3,9 @@ "https://w3id.org/edc/connector/management/v2" ], "@type": "ContractRequest", - "counterPartyAddress": "{{PROVIDER_DSP_URL}}/api/dsp", + "counterPartyAddress": "{{PROVIDER_DSP_URL}}", "counterPartyId": "{{PROVIDER_ID}}", - "protocol": "dataspace-protocol-http", + "protocol": "dataspace-protocol-http:2025-1", "policy": { "@type": "Offer", "@id": "{{OFFER_ID}}", @@ -15,12 +15,12 @@ "obligation": { "action": "use", "constraint": { - "leftOperand": "DataAccess.level", + "leftOperand": "ManufacturerCredential", "operator": "eq", - "rightOperand": "processing" + "rightOperand": "active" } }, - "target": "asset-1" + "target": "asset-2" }, "callbackAddresses": [] } \ No newline at end of file diff --git a/tests/end2end/src/test/resources/transfer-request.json b/tests/end2end/src/test/resources/transfer-request.json index aea7297b4..f4806ce21 100644 --- a/tests/end2end/src/test/resources/transfer-request.json +++ b/tests/end2end/src/test/resources/transfer-request.json @@ -2,13 +2,14 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, + "@type": "TransferRequest", "assetId": "asset-1", - "counterPartyAddress": "{{PROVIDER_DSP_URL}}/api/dsp", + "counterPartyAddress": "{{PROVIDER_DSP_URL}}", "connectorId": "{{PROVIDER_ID}}", "contractId": "{{CONTRACT_ID}}", "dataDestination": { "type": "HttpProxy" }, - "protocol": "dataspace-protocol-http", + "protocol": "dataspace-protocol-http:2025-1", "transferType": "HttpData-PULL" } \ No newline at end of file From 109ed3251b62907f19195552238f16155b080744 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 09:49:37 +0100 Subject: [PATCH 14/22] add DP registration API, fix e2e test pt 2 --- ...rg.eclipse.edc.spi.system.ServiceExtension | 2 +- .../data-plane-registration/build.gradle.kts | 36 ++++ .../api/DataplaneRegistrationExtension.java | 48 +++++ .../DataplaneRegistrationApiController.java | 45 +++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + gradle/libs.versions.toml | 1 + .../application/controlplane-seed.yaml | 6 +- .../application/identityhub-seed.yaml | 2 +- .../application/controlplane-seed.yaml | 189 ++++-------------- .../application/dataplane-config.yaml | 45 +++++ k8s/provider/application/dataplane.yaml | 150 ++++++++++++++ .../application/identityhub-seed.yaml | 2 +- k8s/provider/application/identityhub.yaml | 9 +- k8s/provider/kustomization.yml | 10 +- launchers/controlplane/build.gradle.kts | 1 + settings.gradle.kts | 1 + .../tests/transfer/TransferEndToEndTest.java | 43 ++-- 17 files changed, 409 insertions(+), 182 deletions(-) create mode 100644 extensions/data-plane-registration/build.gradle.kts create mode 100644 extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/DataplaneRegistrationExtension.java create mode 100644 extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java create mode 100644 extensions/data-plane-registration/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 k8s/provider/application/dataplane-config.yaml create mode 100644 k8s/provider/application/dataplane.yaml diff --git a/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 432c0528d..b9313e304 100644 --- a/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/extensions/data-plane-public-api-v2/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -1 +1 @@ -org.eclipse.edc.connector.dataplane.api.DataPlanePublicApiV2Extension +org.eclipse.edc.connector.dataplane.api.DataplaneRegistrationExtension diff --git a/extensions/data-plane-registration/build.gradle.kts b/extensions/data-plane-registration/build.gradle.kts new file mode 100644 index 000000000..f03369094 --- /dev/null +++ b/extensions/data-plane-registration/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * 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 + * + */ + +plugins { + `java-library` + id(libs.plugins.swagger.get().pluginId) +} + +dependencies { + api(libs.edc.spi.http) + api(libs.edc.spi.web) + api(libs.edc.spi.dataplane) + implementation(libs.edc.spi.dataplane.selector) + implementation(libs.edc.lib.util) + implementation(libs.edc.lib.util.dataplane) + implementation(libs.jakarta.rsApi) + +} +edcBuild { + swagger { + apiGroup.set("public-api") + } +} + + diff --git a/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/DataplaneRegistrationExtension.java b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/DataplaneRegistrationExtension.java new file mode 100644 index 000000000..c3c492a8c --- /dev/null +++ b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/DataplaneRegistrationExtension.java @@ -0,0 +1,48 @@ +/* + * 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.connector.dataplane.api; + +import org.eclipse.edc.connector.dataplane.api.controller.DataplaneRegistrationApiController; +import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.ApiContext; + +/** + * This extension provides generic endpoints which are open to public participants of the Dataspace to execute + * requests on the actual data source. + */ +@Extension(value = DataplaneRegistrationExtension.NAME) +public class DataplaneRegistrationExtension implements ServiceExtension { + public static final String NAME = "Data Plane Registration API"; + + @Inject + private WebService webService; + @Inject + private DataPlaneSelectorService selectorService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + webService.registerResource(ApiContext.MANAGEMENT, new DataplaneRegistrationApiController(selectorService)); + } +} diff --git a/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java new file mode 100644 index 000000000..cd2274da7 --- /dev/null +++ b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java @@ -0,0 +1,45 @@ +/* + * 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.connector.dataplane.api.controller; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; +import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/v4beta/dataplanes") +public class DataplaneRegistrationApiController { + + private final DataPlaneSelectorService dataPlaneSelectorService; + + public DataplaneRegistrationApiController(DataPlaneSelectorService dataPlaneSelectorService) { + this.dataPlaneSelectorService = dataPlaneSelectorService; + } + + @POST + public void registerDataplane(DataPlaneInstance instance) { + dataPlaneSelectorService.register(instance.toBuilder().build()) // ugly, but will initialize all internal objects e.g. clock + .orElseThrow(exceptionMapper(DataPlaneInstance.class)); + + } +} diff --git a/extensions/data-plane-registration/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/data-plane-registration/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..b9313e304 --- /dev/null +++ b/extensions/data-plane-registration/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.eclipse.edc.connector.dataplane.api.DataplaneRegistrationExtension diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42b59017a..a8fbacab6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ edc-spi-identity-did = { module = "org.eclipse.edc:identity-did-spi", version.re edc-spi-http = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } edc-spi-dataplane = { module = "org.eclipse.edc:data-plane-spi", version.ref = "edc" } +edc-spi-dataplane-selector = { module = "org.eclipse.edc:data-plane-selector-spi", version.ref = "edc" } # EDC lib dependencies edc-lib-jws2020 = { module = "org.eclipse.edc:jws2020-lib", version.ref = "edc" } diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml index 65ba47edc..39986011e 100644 --- a/k8s/consumer/application/controlplane-seed.yaml +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -3,10 +3,10 @@ apiVersion: batch/v1 kind: Job metadata: - name: controlplane-seed-cel + name: controlplane-seed namespace: consumer labels: - app: controlplane-seed-cel + app: controlplane-seed platform: edcv type: edc-job spec: @@ -14,7 +14,7 @@ spec: template: metadata: labels: - app: controlplane-seed-cel + app: controlplane-seed type: edc-job spec: restartPolicy: OnFailure diff --git a/k8s/consumer/application/identityhub-seed.yaml b/k8s/consumer/application/identityhub-seed.yaml index 6caf3d9a1..833a30e66 100644 --- a/k8s/consumer/application/identityhub-seed.yaml +++ b/k8s/consumer/application/identityhub-seed.yaml @@ -162,7 +162,7 @@ spec: }, { \"type\": \"ProtocolEndpoint\", - \"serviceEndpoint\": \"http://controlplane.consumer.svc.cluster.local:8082/api/dsp\", + \"serviceEndpoint\": \"http://controlplane.consumer.svc.cluster.local:8082/api/dsp/2025-1\", \"id\": \"consumer-dsp\" } ], diff --git a/k8s/provider/application/controlplane-seed.yaml b/k8s/provider/application/controlplane-seed.yaml index 11073eedd..4744ffc32 100644 --- a/k8s/provider/application/controlplane-seed.yaml +++ b/k8s/provider/application/controlplane-seed.yaml @@ -11,16 +11,16 @@ # Metaform Systems, Inc. - initial API and implementation # -## This seed job creates assets, policies and contract definitions provider's control plane via the management API. +## This seed job creates assets, policies and contract definitions in the provider's control plane via the management API. --- apiVersion: batch/v1 kind: Job metadata: - name: controlplane-seed-assets + name: controlplane-seed namespace: provider labels: - app: controlplane-seed-assets + app: controlplane-seed platform: edcv type: edc-job spec: @@ -28,7 +28,7 @@ spec: template: metadata: labels: - app: controlplane-seed-assets + app: controlplane-seed type: edc-job spec: restartPolicy: OnFailure @@ -54,17 +54,23 @@ spec: - | set -e - MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/assets" + ASSETS_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/assets" + POLICIES_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/policydefinitions" + CONTRACTDEFS_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/contractdefinitions" + DATAPLANE_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/dataplanes" # Posts to the management API, treating 409 (already exists) as success. - create_asset() { - HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "${MGMT_URL}" \ + post() { + RESPONSE=$(curl -sS -w "\n%{http_code}" -X "$2" "$1" \ -H "Content-Type: application/json" \ - -d "$1") + -d "$3") + HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) + BODY=$(echo "${RESPONSE}" | sed '$d') if [ "${HTTP_STATUS}" -eq 409 ]; then - echo "⚠ Asset already exists (HTTP 409), skipping." + echo "⚠ Already exists (HTTP 409), skipping." elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" + echo "Response body: ${BODY}" exit 1 fi } @@ -73,7 +79,7 @@ spec: echo "Step 1: Create Asset 1" echo "================================================" - create_asset '{ + post "${ASSETS_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -98,7 +104,7 @@ spec: echo "Step 2: Create Asset 2" echo "================================================" - create_asset '{ + post "${ASSETS_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -120,74 +126,10 @@ spec: echo "" echo "================================================" - echo "Controlplane seeding completed successfully!" - echo "================================================" - - ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: controlplane-seed-policies - namespace: provider - labels: - app: controlplane-seed-policies - platform: edcv - type: edc-job -spec: - backoffLimit: 5 - template: - metadata: - labels: - app: controlplane-seed-policies - type: edc-job - spec: - restartPolicy: OnFailure - initContainers: - - name: wait-for-controlplane - image: curlimages/curl:latest - command: - - sh - - -c - - | - until curl -sf http://controlplane.provider.svc.cluster.local:8080/api/check/readiness; do - echo "Waiting for controlplane to be ready..." - sleep 5 - done - echo "" - echo "Controlplane is ready!" - containers: - - name: seed-policies - image: curlimages/curl:latest - command: - - sh - - -c - - | - set -e - - MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/policydefinitions" - - # Posts a policy definition, treating 409 (already exists) as success. - create_policy() { - RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${MGMT_URL}" \ - -H "Content-Type: application/json" \ - -d "$1") - HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) - BODY=$(echo "${RESPONSE}" | sed '$d') - if [ "${HTTP_STATUS}" -eq 409 ]; then - echo "⚠ Policy already exists (HTTP 409), skipping." - elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then - echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" - echo "Response body: ${BODY}" - exit 1 - fi - } - - echo "================================================" - echo "Step 1: Create Membership Policy" + echo "Step 3: Create Membership Policy" echo "================================================" - create_policy '{ + post "${POLICIES_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -212,10 +154,10 @@ spec: echo "" echo "================================================" - echo "Step 2: Create Manufacturer Policy" + echo "Step 4: Create Manufacturer Policy" echo "================================================" - create_policy '{ + post "${POLICIES_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -240,10 +182,10 @@ spec: echo "" echo "================================================" - echo "Step 3: Create Sensitive Data Policy" + echo "Step 5: Create Sensitive Data Policy" echo "================================================" - create_policy '{ + post "${POLICIES_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -270,72 +212,10 @@ spec: echo "" echo "================================================" - echo "Policy seeding completed successfully!" + echo "Step 6: Create member-and-manufacturer-def" echo "================================================" ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: controlplane-seed-contractdefs - namespace: provider - labels: - app: controlplane-seed-contractdefs - type: edc-job -spec: - backoffLimit: 5 - template: - metadata: - labels: - app: controlplane-seed-contractdefs - type: edc-job - spec: - restartPolicy: OnFailure - initContainers: - - name: wait-for-controlplane - image: curlimages/curl:latest - command: - - sh - - -c - - | - until curl -sf http://controlplane.provider.svc.cluster.local:8080/api/check/readiness; do - echo "Waiting for controlplane to be ready..." - sleep 5 - done - echo "" - echo "Controlplane is ready!" - containers: - - name: seed-contractdefs - image: curlimages/curl:latest - command: - - sh - - -c - - | - set -e - - MGMT_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/contractdefinitions" - - # Posts a contract definition, treating 409 (already exists) as success. - create_contractdef() { - RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "${MGMT_URL}" \ - -H "Content-Type: application/json" \ - -d "$1") - HTTP_STATUS=$(echo "${RESPONSE}" | tail -n1) - BODY=$(echo "${RESPONSE}" | sed '$d') - if [ "${HTTP_STATUS}" -eq 409 ]; then - echo "⚠ Contract definition already exists (HTTP 409), skipping." - elif [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then - echo "✗ Unexpected HTTP status: ${HTTP_STATUS}" - echo "Response body: ${BODY}" - exit 1 - fi - } - - echo "================================================" - echo "Step 1: Create member-and-manufacturer-def" - echo "================================================" - - create_contractdef '{ + post "${CONTRACTDEFS_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -355,10 +235,10 @@ spec: echo "" echo "================================================" - echo "Step 2: Create sensitive-only-def" + echo "Step 7: Create sensitive-only-def" echo "================================================" - create_contractdef '{ + post "${CONTRACTDEFS_URL}" "POST" '{ "@context": [ "https://w3id.org/edc/connector/management/v2" ], @@ -375,8 +255,21 @@ spec: }' echo "✓ sensitive-only-def done" + + echo "" + echo "================================================" + echo "Step 8: Register dataplane" + echo "================================================" + post "${DATAPLANE_URL}" "POST" '{ + "allowedSourceTypes": [ "HttpData", "HttpCertData" ], + "allowedTransferTypes": [ "HttpData-PULL" ], + "url": "http://dataplane.provider.svc.cluster.local:8083/api/control/v1/dataflows" + }' + + echo "✓ dataplane registered" + echo "" echo "================================================" - echo "Contract definition seeding completed successfully!" + echo "Controlplane seeding completed successfully!" echo "================================================" \ No newline at end of file diff --git a/k8s/provider/application/dataplane-config.yaml b/k8s/provider/application/dataplane-config.yaml new file mode 100644 index 000000000..5d44fb98d --- /dev/null +++ b/k8s/provider/application/dataplane-config.yaml @@ -0,0 +1,45 @@ +# +# 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 +# + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dataplane-config + namespace: provider +data: + edc.hostname: "dataplane.provider.svc.cluster.local" + + edc.transfer.proxy.token.verifier.publickey.alias: "dataplane-private" + edc.transfer.proxy.token.signer.privatekey.alias: "dataplane-public" + + edc.dpf.selector.url: "http://controlplane.provider.svc.cluster.local:8083/api/control/v1/dataplanes" + + web.http.port: "8080" + web.http.path: "/api" + web.http.control.port: "8083" + web.http.control.path: "/api/control" + web.http.public.port: "11002" + web.http.public.path: "/api/public" + web.http.certs.port: "8186" + web.http.certs.path: "/api/data" + + edc.vault.hashicorp.url: "http://vault.provider.svc.cluster.local:8200" + edc.vault.hashicorp.token: "root" + + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044" + + edc.datasource.default.url: "jdbc:postgresql://postgres.provider.svc.cluster.local:5432/dataplane" + edc.datasource.default.user: "dp" + edc.datasource.default.password: "dp" + edc.sql.schema.autocreate: "true" diff --git a/k8s/provider/application/dataplane.yaml b/k8s/provider/application/dataplane.yaml new file mode 100644 index 000000000..cc0a0b96a --- /dev/null +++ b/k8s/provider/application/dataplane.yaml @@ -0,0 +1,150 @@ +# +# 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 +# + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dataplane + namespace: provider + labels: + app: dataplane + type: edcv-app +spec: + replicas: 1 + selector: + matchLabels: + app: dataplane + template: + metadata: + labels: + app: dataplane + platform: edcv + type: edcv-app + spec: + containers: + - name: dataplane + image: ghcr.io/metaform/jad/dataplane:latest + imagePullPolicy: Always + envFrom: + - configMapRef: + name: dataplane-config + ports: + - containerPort: 1044 + name: debug-port + livenessProbe: + httpGet: + path: /api/check/liveness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 30 + readinessProbe: + httpGet: + path: /api/check/readiness + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 30 + startupProbe: + httpGet: + path: /api/check/startup + port: 8080 + failureThreshold: 10 + periodSeconds: 5 + timeoutSeconds: 30 + +--- +apiVersion: v1 +kind: Service +metadata: + name: dataplane + namespace: provider +spec: + type: ClusterIP + selector: + app: dataplane + ports: + - name: health + port: 8080 + targetPort: 8080 + - name: control + port: 8083 + targetPort: 8083 + - name: certs + port: 8186 + targetPort: 8186 + - name: public + port: 11002 + targetPort: 11002 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httproute-dataplane + namespace: provider +spec: + parentRefs: + - name: provider-gateway + namespace: provider + hostnames: + - dp.provider.localhost + rules: + # /dp/public → public port 11002 + - matches: + - path: + type: PathPrefix + value: /public + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: dataplane + port: 11002 + weight: 1 + + # /app/internal → control port 8083 + - matches: + - path: + type: PathPrefix + value: /app/internal + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: dataplane + port: 8083 + weight: 1 + + # /app/public → certs port 8186 + - matches: + - path: + type: PathPrefix + value: /app/public + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: dataplane + port: 8186 + weight: 1 \ No newline at end of file diff --git a/k8s/provider/application/identityhub-seed.yaml b/k8s/provider/application/identityhub-seed.yaml index 0c9a43826..068cc237f 100644 --- a/k8s/provider/application/identityhub-seed.yaml +++ b/k8s/provider/application/identityhub-seed.yaml @@ -162,7 +162,7 @@ spec: }, { \"type\": \"ProtocolEndpoint\", - \"serviceEndpoint\": \"http://controlplane.provider.svc.cluster.local:8082/api/dsp\", + \"serviceEndpoint\": \"http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1\", \"id\": \"provider-dsp\" } ], diff --git a/k8s/provider/application/identityhub.yaml b/k8s/provider/application/identityhub.yaml index b5b66627e..fdd976020 100644 --- a/k8s/provider/application/identityhub.yaml +++ b/k8s/provider/application/identityhub.yaml @@ -74,15 +74,18 @@ spec: selector: app: identityhub ports: - - port: 7082 - targetPort: 7082 - name: creds-port - port: 1044 targetPort: 1044 name: debug + - port: 7080 + targetPort: 7080 + name: web - port: 7081 targetPort: 7081 name: identity-api + - port: 7082 + targetPort: 7082 + name: creds-port - port: 7083 targetPort: 7083 name: did diff --git a/k8s/provider/kustomization.yml b/k8s/provider/kustomization.yml index 89a29f07d..5c6cafd82 100644 --- a/k8s/provider/kustomization.yml +++ b/k8s/provider/kustomization.yml @@ -16,9 +16,11 @@ resources: - base/vault.yaml - base/gateway.yaml - base/postgres.yaml - - application/controlplane-config.yaml - - application/controlplane.yaml - - application/controlplane-seed.yaml - application/identityhub-config.yaml - application/identityhub.yaml - - application/identityhub-seed.yaml \ No newline at end of file + - application/identityhub-seed.yaml + - application/dataplane-config.yaml + - application/dataplane.yaml + - application/controlplane-config.yaml + - application/controlplane.yaml + - application/controlplane-seed.yaml \ No newline at end of file diff --git a/launchers/controlplane/build.gradle.kts b/launchers/controlplane/build.gradle.kts index 322b80324..476ae7ed6 100644 --- a/launchers/controlplane/build.gradle.kts +++ b/launchers/controlplane/build.gradle.kts @@ -19,6 +19,7 @@ plugins { } dependencies { + runtimeOnly(project(":extensions:data-plane-registration")) runtimeOnly(libs.edc.api.cel.v5) runtimeOnly(libs.edc.core.cel) runtimeOnly(libs.edc.cel.store.sql) diff --git a/settings.gradle.kts b/settings.gradle.kts index 5354c678b..750069f62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ rootProject.name = "mvd" include(":tests:end2end") include(":extensions:data-plane-public-api-v2") +include(":extensions:data-plane-registration") // launcher modules include(":launchers:identity-hub") diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java index a970c8ef1..d6e232129 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java @@ -34,6 +34,7 @@ import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -62,7 +63,7 @@ public class TransferEndToEndTest { // DID of the provider company private static final String PROVIDER_ID = "did:web:identityhub.provider.svc.cluster.local%3A7083:provider"; // public API endpoint of the provider-qna connector, goes through the ingress controller - private static final String PROVIDER_PUBLIC_URL = "http://127.0.0.1/provider-qna/public"; + private static final String PROVIDER_PUBLIC_URL = "http://dp.provider.localhost:8080/public"; private static final String PROVIDER_MANAGEMENT_URL = "http://cp.provider.localhost:8080"; private final TypeTransformerRegistry transformerRegistry = new TypeTransformerRegistryImpl(); @@ -102,23 +103,23 @@ public String fromIri(String s) { void transferData_hasPermission_shouldTransferData() { System.out.println("Waiting for Provider dataplane to come online"); // wait until provider's dataplane is available -// await().atMost(TEST_TIMEOUT_DURATION) -// .pollDelay(TEST_POLL_DELAY) -// .untilAsserted(() -> { -// var jp = baseRequest() -// .get(PROVIDER_MANAGEMENT_URL + "/api/mgmt/v4beta/dataplanes") -// .then() -// .statusCode(200) -// .log().ifValidationFails() -// .extract().body().jsonPath(); -// -// var state = jp.getString("state"); -// assertThat(state).isEqualTo("[AVAILABLE]"); -// }); -// -// System.out.println("Provider dataplane is online, fetching catalog"); - - var queryBody = Json.createObjectBuilder() + await().atMost(TEST_TIMEOUT_DURATION) + .pollDelay(TEST_POLL_DELAY) + .untilAsserted(() -> { + var jp = baseRequest() + .get(PROVIDER_MANAGEMENT_URL + "/api/mgmt/v4beta/dataplanes") + .then() + .statusCode(200) + .log().ifValidationFails() + .extract().body().jsonPath(); + + var state = jp.getString("state"); + assertThat(state).isEqualTo("[REGISTERED]"); + }); + + System.out.println("Provider dataplane is online, fetching catalog"); + + var catalogRequestBody = Json.createObjectBuilder() .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/connector/management/v2")) .add("@type", "CatalogRequest") .add("counterPartyId", PROVIDER_ID) @@ -132,7 +133,7 @@ void transferData_hasPermission_shouldTransferData() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var res = baseRequest() - .body(queryBody) + .body(catalogRequestBody) .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/catalog/request") .then() .log().ifValidationFails() @@ -201,8 +202,7 @@ void transferData_hasPermission_shouldTransferData() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .body(queryBody) - .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/transferprocesses/request") + .get(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/transferprocesses/%s/state".formatted(transferProcessId)) .then() .statusCode(200) .extract().body().jsonPath(); @@ -244,6 +244,7 @@ void transferData_hasPermission_shouldTransferData() { assertThat(response).isNotEmpty(); } + @Disabled @DisplayName("Tests a failing End-to-End contract negotiation because of an unfulfilled policy") @Test void transferData_doesNotHavePermission_shouldTerminate() { From 0f7c517c9df1d036b2086f75d2f8fec86be573f9 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 10:19:10 +0100 Subject: [PATCH 15/22] fix tests, remove old terraform stuff --- deployment/assets/consumer_private.pem | 5 - deployment/assets/consumer_public.pem | 4 - .../consumer/dataprocessor-credential.json | 39 - .../k8s/consumer/dataprocessor_vc.json | 24 - .../k8s/consumer/membership-credential.json | 41 - .../k8s/consumer/membership_vc.json | 31 - .../provider/dataprocessor-credential.json | 39 - .../k8s/provider/dataprocessor_vc.json | 24 - .../k8s/provider/membership-credential.json | 43 - .../k8s/provider/membership_vc.json | 31 - .../consumer/dataprocessor-credential.json | 39 - .../local/consumer/membership-credential.json | 41 - .../consumer/unsigned/dataprocessor_vc.json | 24 - .../consumer/unsigned/membership_vc.json | 31 - .../provider/dataprocessor-credential.json | 39 - .../local/provider/membership-credential.json | 43 - .../provider/unsigned/dataprocessor_vc.json | 24 - .../provider/unsigned/membership_vc.json | 31 - deployment/assets/env/consumer_connector.env | 38 - .../assets/env/consumer_identityhub.env | 19 - deployment/assets/env/issuerservice.env | 15 - .../assets/env/provider_catalogserver.env | 20 - .../env/provider_connector_manufacturing.env | 38 - .../assets/env/provider_connector_qna.env | 38 - .../assets/env/provider_identityhub.env | 19 - deployment/assets/issuer/did.docker.json | 26 - deployment/assets/issuer/did.k8s.json | 26 - deployment/assets/issuer/nginx.conf | 9 - deployment/assets/issuer_private.pem | 3 - deployment/assets/issuer_public.pem | 3 - .../assets/participants/participants.k8s.json | 4 - .../participants/participants.local.json | 4 - deployment/assets/provider_private.pem | 5 - deployment/assets/provider_public.pem | 4 - deployment/consumer.tf | 83 - deployment/issuer.tf | 68 - deployment/issuer_nginx.tf | 110 - deployment/kind.config.yaml | 37 - deployment/main.tf | 51 - .../modules/catalog-server/catalog-server.tf | 155 - deployment/modules/catalog-server/ingress.tf | 59 - deployment/modules/catalog-server/outputs.tf | 37 - deployment/modules/catalog-server/services.tf | 47 - .../modules/catalog-server/variables.tf | 117 - deployment/modules/connector/controlplane.tf | 185 - deployment/modules/connector/dataplane.tf | 137 - deployment/modules/connector/ingress.tf | 99 - deployment/modules/connector/outputs.tf | 38 - deployment/modules/connector/services.tf | 76 - deployment/modules/connector/variables.tf | 115 - deployment/modules/identity-hub/ingress.tf | 81 - deployment/modules/identity-hub/main.tf | 171 - deployment/modules/identity-hub/outputs.tf | 42 - deployment/modules/identity-hub/services.tf | 46 - deployment/modules/identity-hub/variables.tf | 115 - deployment/modules/issuer/ingress.tf | 87 - deployment/modules/issuer/main.tf | 159 - deployment/modules/issuer/services.tf | 53 - deployment/modules/issuer/variables.tf | 82 - deployment/modules/postgres/main.tf | 131 - deployment/modules/postgres/outputs.tf | 28 - deployment/modules/postgres/variables.tf | 29 - deployment/modules/vault/variables.tf | 44 - deployment/modules/vault/vault-values.yaml | 22 - deployment/modules/vault/vault.tf | 70 - deployment/outputs.tf | 31 - .../postman/MVD K8S.postman_environment.json | 129 - ...Local Development.postman_environment.json | 105 - .../postman/MVD.postman_collection.json | 6218 ----------------- deployment/postman/http-client.env.json | 9 - deployment/provider.tf | 163 - deployment/variables.tf | 32 - .../CredentialIssuanceEndToEndTest.java | 39 +- 73 files changed, 24 insertions(+), 10100 deletions(-) delete mode 100644 deployment/assets/consumer_private.pem delete mode 100644 deployment/assets/consumer_public.pem delete mode 100644 deployment/assets/credentials/k8s/consumer/dataprocessor-credential.json delete mode 100644 deployment/assets/credentials/k8s/consumer/dataprocessor_vc.json delete mode 100644 deployment/assets/credentials/k8s/consumer/membership-credential.json delete mode 100644 deployment/assets/credentials/k8s/consumer/membership_vc.json delete mode 100644 deployment/assets/credentials/k8s/provider/dataprocessor-credential.json delete mode 100644 deployment/assets/credentials/k8s/provider/dataprocessor_vc.json delete mode 100644 deployment/assets/credentials/k8s/provider/membership-credential.json delete mode 100644 deployment/assets/credentials/k8s/provider/membership_vc.json delete mode 100644 deployment/assets/credentials/local/consumer/dataprocessor-credential.json delete mode 100644 deployment/assets/credentials/local/consumer/membership-credential.json delete mode 100644 deployment/assets/credentials/local/consumer/unsigned/dataprocessor_vc.json delete mode 100644 deployment/assets/credentials/local/consumer/unsigned/membership_vc.json delete mode 100644 deployment/assets/credentials/local/provider/dataprocessor-credential.json delete mode 100644 deployment/assets/credentials/local/provider/membership-credential.json delete mode 100644 deployment/assets/credentials/local/provider/unsigned/dataprocessor_vc.json delete mode 100644 deployment/assets/credentials/local/provider/unsigned/membership_vc.json delete mode 100644 deployment/assets/env/consumer_connector.env delete mode 100644 deployment/assets/env/consumer_identityhub.env delete mode 100644 deployment/assets/env/issuerservice.env delete mode 100644 deployment/assets/env/provider_catalogserver.env delete mode 100644 deployment/assets/env/provider_connector_manufacturing.env delete mode 100644 deployment/assets/env/provider_connector_qna.env delete mode 100644 deployment/assets/env/provider_identityhub.env delete mode 100644 deployment/assets/issuer/did.docker.json delete mode 100644 deployment/assets/issuer/did.k8s.json delete mode 100644 deployment/assets/issuer/nginx.conf delete mode 100644 deployment/assets/issuer_private.pem delete mode 100644 deployment/assets/issuer_public.pem delete mode 100644 deployment/assets/participants/participants.k8s.json delete mode 100644 deployment/assets/participants/participants.local.json delete mode 100644 deployment/assets/provider_private.pem delete mode 100644 deployment/assets/provider_public.pem delete mode 100644 deployment/consumer.tf delete mode 100644 deployment/issuer.tf delete mode 100644 deployment/issuer_nginx.tf delete mode 100644 deployment/kind.config.yaml delete mode 100644 deployment/main.tf delete mode 100644 deployment/modules/catalog-server/catalog-server.tf delete mode 100644 deployment/modules/catalog-server/ingress.tf delete mode 100644 deployment/modules/catalog-server/outputs.tf delete mode 100644 deployment/modules/catalog-server/services.tf delete mode 100644 deployment/modules/catalog-server/variables.tf delete mode 100644 deployment/modules/connector/controlplane.tf delete mode 100644 deployment/modules/connector/dataplane.tf delete mode 100644 deployment/modules/connector/ingress.tf delete mode 100644 deployment/modules/connector/outputs.tf delete mode 100644 deployment/modules/connector/services.tf delete mode 100644 deployment/modules/connector/variables.tf delete mode 100644 deployment/modules/identity-hub/ingress.tf delete mode 100644 deployment/modules/identity-hub/main.tf delete mode 100644 deployment/modules/identity-hub/outputs.tf delete mode 100644 deployment/modules/identity-hub/services.tf delete mode 100644 deployment/modules/identity-hub/variables.tf delete mode 100644 deployment/modules/issuer/ingress.tf delete mode 100644 deployment/modules/issuer/main.tf delete mode 100644 deployment/modules/issuer/services.tf delete mode 100644 deployment/modules/issuer/variables.tf delete mode 100644 deployment/modules/postgres/main.tf delete mode 100644 deployment/modules/postgres/outputs.tf delete mode 100644 deployment/modules/postgres/variables.tf delete mode 100644 deployment/modules/vault/variables.tf delete mode 100644 deployment/modules/vault/vault-values.yaml delete mode 100644 deployment/modules/vault/vault.tf delete mode 100644 deployment/outputs.tf delete mode 100644 deployment/postman/MVD K8S.postman_environment.json delete mode 100644 deployment/postman/MVD Local Development.postman_environment.json delete mode 100644 deployment/postman/MVD.postman_collection.json delete mode 100644 deployment/postman/http-client.env.json delete mode 100644 deployment/provider.tf delete mode 100644 deployment/variables.tf diff --git a/deployment/assets/consumer_private.pem b/deployment/assets/consumer_private.pem deleted file mode 100644 index 81c28bac2..000000000 --- a/deployment/assets/consumer_private.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIARDUGJgKy1yzxkueIJ1k3MPUWQ/tbQWQNqW6TjyHpdcoAoGCCqGSM49 -AwEHoUQDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5roYnkAXuqCYfNK3ex+hMWFuiX -GUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== ------END EC PRIVATE KEY----- \ No newline at end of file diff --git a/deployment/assets/consumer_public.pem b/deployment/assets/consumer_public.pem deleted file mode 100644 index 977a19576..000000000 --- a/deployment/assets/consumer_public.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5r -oYnkAXuqCYfNK3ex+hMWFuiXGUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/deployment/assets/credentials/k8s/consumer/dataprocessor-credential.json b/deployment/assets/credentials/k8s/consumer/dataprocessor-credential.json deleted file mode 100644 index f7c8f50dc..000000000 --- a/deployment/assets/credentials/k8s/consumer/dataprocessor-credential.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b15", - "participantContextId": "did:web:consumer-identityhub%3A7083:consumer", - "timestamp": 1700659822500, - "issuerId": "did:web:dataspace-issuer", - "holderId": "did:web:consumer-identityhub%3A7083:consumer", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "format": "VC1_0_JWT", - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLWlkZW50aXR5aHViJTNBNzA4MzphbGljZSIsInN1YiI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIlM0E3MDgzOmFsaWNlIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIsImh0dHBzOi8vd3d3LnczLm9yZy9ucy9kaWQvdjEiLHsibXZkLWNyZWRlbnRpYWxzIjoiaHR0cHM6Ly93M2lkLm9yZy9tdmQvY3JlZGVudGlhbHMvIiwiY29udHJhY3RWZXJzaW9uIjoibXZkLWNyZWRlbnRpYWxzOmNvbnRyYWN0VmVyc2lvbiIsImxldmVsIjoibXZkLWNyZWRlbnRpYWxzOmxldmVsIn1dLCJpZCI6Imh0dHA6Ly9vcmcueW91cmRhdGFzcGFjZS5jb20vY3JlZGVudGlhbHMvMjM0NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEYXRhUHJvY2Vzc29yQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA4LTE4VDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjpjb25zdW1lci1pZGVudGl0eWh1YiUzQTcwODM6Y29uc3VtZXIiLCJjb250cmFjdFZlcnNpb24iOiIxLjAuMCIsImxldmVsIjoicHJvY2Vzc2luZyJ9fSwiaWF0IjoxNzQ4ODQ0OTE5fQ.Asd_5HEu-UaV3bSZ3DlkIlI5yiAik18JcAtKwK6HVx3MAW5uR907lEJfgdO29eHfTR9_qiHG5OitXYCpL_sxBQ", - "credential": { - "credentialSubject": [ - { - "claims": { - "id": "did:web:consumer-identityhub%3A7083:consumer", - "contractVersion": "1.0.0", - "level": "processing" - } - } - ], - "id": "http://org.yourdataspace.com/credentials/1235", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": { - "id": "did:web:dataspace-issuer", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/k8s/consumer/dataprocessor_vc.json b/deployment/assets/credentials/k8s/consumer/dataprocessor_vc.json deleted file mode 100644 index fb5154731..000000000 --- a/deployment/assets/credentials/k8s/consumer/dataprocessor_vc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "contractVersion": "mvd-credentials:contractVersion", - "level": "mvd-credentials:level" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": "did:web:dataspace-issuer", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:consumer-identityhub%3A7083:consumer", - "contractVersion": "1.0.0", - "level": "processing" - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/k8s/consumer/membership-credential.json b/deployment/assets/credentials/k8s/consumer/membership-credential.json deleted file mode 100644 index c8d45368d..000000000 --- a/deployment/assets/credentials/k8s/consumer/membership-credential.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b14", - "participantContextId": "did:web:consumer-identityhub%3A7083:consumer", - "timestamp": 1700659822500, - "issuerId": "did:web:dataspace-issuer", - "holderId": "did:web:consumer-identityhub%3A7083:consumer", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLWlkZW50aXR5aHViJTNBNzA4MzphbGljZSIsInN1YiI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIlM0E3MDgzOmFsaWNlIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIsImh0dHBzOi8vd3d3LnczLm9yZy9ucy9kaWQvdjEiLHsibXZkLWNyZWRlbnRpYWxzIjoiaHR0cHM6Ly93M2lkLm9yZy9tdmQvY3JlZGVudGlhbHMvIiwibWVtYmVyc2hpcCI6Im12ZC1jcmVkZW50aWFsczptZW1iZXJzaGlwIiwibWVtYmVyc2hpcFR5cGUiOiJtdmQtY3JlZGVudGlhbHM6bWVtYmVyc2hpcFR5cGUiLCJ3ZWJzaXRlIjoibXZkLWNyZWRlbnRpYWxzOndlYnNpdGUiLCJjb250YWN0IjoibXZkLWNyZWRlbnRpYWxzOmNvbnRhY3QiLCJzaW5jZSI6Im12ZC1jcmVkZW50aWFsczpzaW5jZSJ9XSwiaWQiOiJodHRwOi8vb3JnLnlvdXJkYXRhc3BhY2UuY29tL2NyZWRlbnRpYWxzLzIzNDciLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiTWVtYmVyc2hpcENyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOndlYjpkYXRhc3BhY2UtaXNzdWVyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wOC0xOFQwMDowMDowMFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIlM0E3MDgzOmNvbnN1bWVyIiwibWVtYmVyc2hpcCI6eyJtZW1iZXJzaGlwVHlwZSI6IkZ1bGxNZW1iZXIiLCJ3ZWJzaXRlIjoid3d3LndoYXRldmVyLmNvbSIsImNvbnRhY3QiOiJmaXp6LmJ1enpAd2hhdGV2ZXIuY29tIiwic2luY2UiOiIyMDIzLTAxLTAxVDAwOjAwOjAwWiJ9fX0sImlhdCI6MTc0ODg0NDkxOX0.xcb9qKJ_BGGj_KvSM9lZIdJW01FSdDjALXxhmH8CehkOPy2nXGnWKIbjHJZmW60NtU7kqRC23THU7OWFs28EDw", - "format": "VC1_0_JWT", - "credential": { - "credentialSubject": [ - { - "claims": { - "membershipType": "FullMember", - "website": "www.some-other-website.com", - "contact": "bar.baz@company.com", - "since": "2023-01-01T00:00:00Z" - }, - "id": "did:web:consumer-identityhub%3A7083:consumer" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": { - "id": "did:web:dataspace-issuer", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/k8s/consumer/membership_vc.json b/deployment/assets/credentials/k8s/consumer/membership_vc.json deleted file mode 100644 index d3f4ae745..000000000 --- a/deployment/assets/credentials/k8s/consumer/membership_vc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "membership": "mvd-credentials:membership", - "membershipType": "mvd-credentials:membershipType", - "website": "mvd-credentials:website", - "contact": "mvd-credentials:contact", - "since": "mvd-credentials:since" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": "did:web:dataspace-issuer", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:consumer-identityhub%3A7083:consumer", - "membership": { - "membershipType": "FullMember", - "website": "www.whatever.com", - "contact": "fizz.buzz@whatever.com", - "since": "2023-01-01T00:00:00Z" - } - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/k8s/provider/dataprocessor-credential.json b/deployment/assets/credentials/k8s/provider/dataprocessor-credential.json deleted file mode 100644 index 7ed5bee2c..000000000 --- a/deployment/assets/credentials/k8s/provider/dataprocessor-credential.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1ca7", - "participantContextId": "did:web:provider-identityhub%3A7083:provider", - "timestamp": 1700659822500, - "issuerId": "did:web:dataspace-issuer", - "holderId": "did:web:provider-identityhub%3A7083:provider", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "format": "VC1_0_JWT", - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJhdWQiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJzdWIiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvc3VpdGVzL2p3cy0yMDIwL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnL25zL2RpZC92MSIseyJtdmQtY3JlZGVudGlhbHMiOiJodHRwczovL3czaWQub3JnL212ZC9jcmVkZW50aWFscy8iLCJjb250cmFjdFZlcnNpb24iOiJtdmQtY3JlZGVudGlhbHM6Y29udHJhY3RWZXJzaW9uIiwibGV2ZWwiOiJtdmQtY3JlZGVudGlhbHM6bGV2ZWwifV0sImlkIjoiaHR0cDovL29yZy55b3VyZGF0YXNwYWNlLmNvbS9jcmVkZW50aWFscy8yMzQ3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRhdGFQcm9jZXNzb3JDcmVkZW50aWFsIl0sImlzc3VlciI6ImRpZDp3ZWI6ZGF0YXNwYWNlLWlzc3VlciIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDgtMThUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpwcm92aWRlciIsImxldmVsIjoicHJvY2Vzc2luZyIsImNvbnRyYWN0VmVyc2lvbiI6IjEuMC4wIn19LCJpYXQiOjE3NDg4NDQ5MTl9.lgSIzaPA9mm1LTEssDlfG2bcKUyhjWfjl85yEMHcKxAjl3kyFw1lBSokCR85f2bm-ZBHiAfCh9M9W1jixjPTCg", - "credential": { - "credentialSubject": [ - { - "claims": { - "id": "did:web:provider-identityhub%3A7083:provider", - "contractVersion": "1.0.0", - "level": "processing" - } - } - ], - "id": "http://org.yourdataspace.com/credentials/1265", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": { - "id": "did:web:dataspace-issuer", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/k8s/provider/dataprocessor_vc.json b/deployment/assets/credentials/k8s/provider/dataprocessor_vc.json deleted file mode 100644 index ff3160ebb..000000000 --- a/deployment/assets/credentials/k8s/provider/dataprocessor_vc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "contractVersion": "mvd-credentials:contractVersion", - "level": "mvd-credentials:level" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": "did:web:dataspace-issuer", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:provider-identityhub%3A7083:provider", - "level": "processing", - "contractVersion": "1.0.0" - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/k8s/provider/membership-credential.json b/deployment/assets/credentials/k8s/provider/membership-credential.json deleted file mode 100644 index 076ef60e0..000000000 --- a/deployment/assets/credentials/k8s/provider/membership-credential.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b14", - "participantContextId": "did:web:provider-identityhub%3A7083:provider", - "timestamp": 1700659822500, - "issuerId": "did:web:dataspace-issuer", - "holderId": "did:web:provider-identityhub%3A7083:provider", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJhdWQiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJzdWIiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvc3VpdGVzL2p3cy0yMDIwL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnL25zL2RpZC92MSIseyJtdmQtY3JlZGVudGlhbHMiOiJodHRwczovL3czaWQub3JnL212ZC9jcmVkZW50aWFscy8iLCJtZW1iZXJzaGlwIjoibXZkLWNyZWRlbnRpYWxzOm1lbWJlcnNoaXAiLCJtZW1iZXJzaGlwVHlwZSI6Im12ZC1jcmVkZW50aWFsczptZW1iZXJzaGlwVHlwZSIsIndlYnNpdGUiOiJtdmQtY3JlZGVudGlhbHM6d2Vic2l0ZSIsImNvbnRhY3QiOiJtdmQtY3JlZGVudGlhbHM6Y29udGFjdCIsInNpbmNlIjoibXZkLWNyZWRlbnRpYWxzOnNpbmNlIn1dLCJpZCI6Imh0dHA6Ly9vcmcueW91cmRhdGFzcGFjZS5jb20vY3JlZGVudGlhbHMvMjM0NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJNZW1iZXJzaGlwQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA4LTE4VDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjpwcm92aWRlci1pZGVudGl0eWh1YiUzQTcwODM6cHJvdmlkZXIiLCJtZW1iZXJzaGlwIjp7Im1lbWJlcnNoaXBUeXBlIjoiRnVsbE1lbWJlciIsIndlYnNpdGUiOiJ3d3cud2hhdGV2ZXIuY29tIiwiY29udGFjdCI6Im1peC5tYXhAd2hhdGV2ZXIuY29tIiwic2luY2UiOiIyMDIzLTAxLTAxVDAwOjAwOjAwWiJ9fX0sImlhdCI6MTc0ODg0NDkxOH0.iX84wIF6unwmOWPtyRHAYv-YaoDSTzHl1ioZcfa-Y6aMGzbgD4EDhjKY9syR5mdYYIvqs__cAN-d3MOKbMgjDA", - "format": "VC1_0_JWT", - "credential": { - "credentialSubject": [ - { - "claims": { - "membership": { - "membershipType": "FullMember", - "website": "www.company-website.com", - "contact": "max.mustermann@company.com", - "since": "2023-05-08T00:00:00Z" - } - }, - "id": "did:web:provider-identityhub%3A7083:provider" - } - ], - "id": "http://org.yourdataspace.com/credentials/1234", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": { - "id": "did:web:dataspace-issuer", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/k8s/provider/membership_vc.json b/deployment/assets/credentials/k8s/provider/membership_vc.json deleted file mode 100644 index 4cf4ec500..000000000 --- a/deployment/assets/credentials/k8s/provider/membership_vc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "membership": "mvd-credentials:membership", - "membershipType": "mvd-credentials:membershipType", - "website": "mvd-credentials:website", - "contact": "mvd-credentials:contact", - "since": "mvd-credentials:since" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": "did:web:dataspace-issuer", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:provider-identityhub%3A7083:provider", - "membership": { - "membershipType": "FullMember", - "website": "www.whatever.com", - "contact": "mix.max@whatever.com", - "since": "2023-01-01T00:00:00Z" - } - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/local/consumer/dataprocessor-credential.json b/deployment/assets/credentials/local/consumer/dataprocessor-credential.json deleted file mode 100644 index 934f2d850..000000000 --- a/deployment/assets/credentials/local/consumer/dataprocessor-credential.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b15", - "participantContextId": "did:web:localhost%3A7083", - "timestamp": 1700659822500, - "issuerId": "did:web:localhost%3A9876", - "holderId": "did:web:localhost%3A7093", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "format": "VC1_0_JWT", - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLWlkZW50aXR5aHViJTNBNzA4MzphbGljZSIsInN1YiI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIlM0E3MDgzOmFsaWNlIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIsImh0dHBzOi8vd3d3LnczLm9yZy9ucy9kaWQvdjEiLHsibXZkLWNyZWRlbnRpYWxzIjoiaHR0cHM6Ly93M2lkLm9yZy9tdmQvY3JlZGVudGlhbHMvIiwiY29udHJhY3RWZXJzaW9uIjoibXZkLWNyZWRlbnRpYWxzOmNvbnRyYWN0VmVyc2lvbiIsImxldmVsIjoibXZkLWNyZWRlbnRpYWxzOmxldmVsIn1dLCJpZCI6Imh0dHA6Ly9vcmcueW91cmRhdGFzcGFjZS5jb20vY3JlZGVudGlhbHMvMjM0NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEYXRhUHJvY2Vzc29yQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA4LTE4VDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgzIiwiY29udHJhY3RWZXJzaW9uIjoiMS4wLjAiLCJsZXZlbCI6InByb2Nlc3NpbmcifX0sImlhdCI6MTc0ODg0NDkxOX0.B3ZjHNsiOhuiv78uv4hu08LyA9gZrciMhKOHsC9CV99_KesoWQAjrsg2bJd2b3QQguLoR0C3S3u-9tcYvmB1Cg", - "credential": { - "credentialSubject": [ - { - "claims": { - "id": "did:web:localhost%3A7083", - "contractVersion": "1.0.0", - "level": "processing" - } - } - ], - "id": "http://org.yourdataspace.com/credentials/1235", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": { - "id": "did:web:localhost%3A9876", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/local/consumer/membership-credential.json b/deployment/assets/credentials/local/consumer/membership-credential.json deleted file mode 100644 index 95ce92cd9..000000000 --- a/deployment/assets/credentials/local/consumer/membership-credential.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b14", - "participantContextId": "did:web:localhost%3A7083", - "timestamp": 1700659822500, - "issuerId": "did:web:localhost%3A9876", - "holderId": "did:web:localhost%3A7083", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJhdWQiOiJkaWQ6d2ViOmNvbnN1bWVyLWlkZW50aXR5aHViJTNBNzA4MzphbGljZSIsInN1YiI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIlM0E3MDgzOmFsaWNlIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIsImh0dHBzOi8vd3d3LnczLm9yZy9ucy9kaWQvdjEiLHsibXZkLWNyZWRlbnRpYWxzIjoiaHR0cHM6Ly93M2lkLm9yZy9tdmQvY3JlZGVudGlhbHMvIiwibWVtYmVyc2hpcCI6Im12ZC1jcmVkZW50aWFsczptZW1iZXJzaGlwIiwibWVtYmVyc2hpcFR5cGUiOiJtdmQtY3JlZGVudGlhbHM6bWVtYmVyc2hpcFR5cGUiLCJ3ZWJzaXRlIjoibXZkLWNyZWRlbnRpYWxzOndlYnNpdGUiLCJjb250YWN0IjoibXZkLWNyZWRlbnRpYWxzOmNvbnRhY3QiLCJzaW5jZSI6Im12ZC1jcmVkZW50aWFsczpzaW5jZSJ9XSwiaWQiOiJodHRwOi8vb3JnLnlvdXJkYXRhc3BhY2UuY29tL2NyZWRlbnRpYWxzLzIzNDciLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiTWVtYmVyc2hpcENyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOndlYjpsb2NhbGhvc3QlM0E5ODc2IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wOC0xOFQwMDowMDowMFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBNzA4MyIsIm1lbWJlcnNoaXAiOnsibWVtYmVyc2hpcFR5cGUiOiJGdWxsTWVtYmVyIiwid2Vic2l0ZSI6Ind3dy53aGF0ZXZlci5jb20iLCJjb250YWN0IjoibWl4Lm1heEB3aGF0ZXZlci5jb20iLCJzaW5jZSI6IjIwMjMtMDEtMDFUMDA6MDA6MDBaIn19fSwiaWF0IjoxNzQ4ODQ0OTE5fQ.xnb1qnjEUpSAzFlJT9krVkW8y7MffVJL7xhfimLEV2ADYtRw_94LvcYuv-eFwMcOEMtNzfWj4MRoM2IslI5rBw", - "format": "VC1_0_JWT", - "credential": { - "credentialSubject": [ - { - "claims": { - "membershipType": "FullMember", - "website": "www.some-other-website.com", - "contact": "bar.baz@company.com", - "since": "2023-01-01T00:00:00Z" - }, - "id": "did:web:localhost%3A7083" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": { - "id": "did:web:localhost%3A9876", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/local/consumer/unsigned/dataprocessor_vc.json b/deployment/assets/credentials/local/consumer/unsigned/dataprocessor_vc.json deleted file mode 100644 index e65876bf7..000000000 --- a/deployment/assets/credentials/local/consumer/unsigned/dataprocessor_vc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "contractVersion": "mvd-credentials:contractVersion", - "level": "mvd-credentials:level" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": "did:web:localhost%3A9876", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:localhost%3A7083", - "contractVersion": "1.0.0", - "level": "processing" - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/local/consumer/unsigned/membership_vc.json b/deployment/assets/credentials/local/consumer/unsigned/membership_vc.json deleted file mode 100644 index 65b1b56da..000000000 --- a/deployment/assets/credentials/local/consumer/unsigned/membership_vc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "membership": "mvd-credentials:membership", - "membershipType": "mvd-credentials:membershipType", - "website": "mvd-credentials:website", - "contact": "mvd-credentials:contact", - "since": "mvd-credentials:since" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": "did:web:localhost%3A9876", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:localhost%3A7083", - "membership": { - "membershipType": "FullMember", - "website": "www.whatever.com", - "contact": "mix.max@whatever.com", - "since": "2023-01-01T00:00:00Z" - } - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/local/provider/dataprocessor-credential.json b/deployment/assets/credentials/local/provider/dataprocessor-credential.json deleted file mode 100644 index aadbef8fe..000000000 --- a/deployment/assets/credentials/local/provider/dataprocessor-credential.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1ca7", - "participantContextId": "did:web:localhost%3A7093", - "timestamp": 1700659822500, - "issuerId": "did:web:localhost%3A9876", - "holderId": "did:web:localhost%3A7093", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "format": "VC1_0_JWT", - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJhdWQiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJzdWIiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvc3VpdGVzL2p3cy0yMDIwL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnL25zL2RpZC92MSIseyJtdmQtY3JlZGVudGlhbHMiOiJodHRwczovL3czaWQub3JnL212ZC9jcmVkZW50aWFscy8iLCJjb250cmFjdFZlcnNpb24iOiJtdmQtY3JlZGVudGlhbHM6Y29udHJhY3RWZXJzaW9uIiwibGV2ZWwiOiJtdmQtY3JlZGVudGlhbHM6bGV2ZWwifV0sImlkIjoiaHR0cDovL29yZy55b3VyZGF0YXNwYWNlLmNvbS9jcmVkZW50aWFscy8yMzQ3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkRhdGFQcm9jZXNzb3JDcmVkZW50aWFsIl0sImlzc3VlciI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBOTg3NiIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDgtMThUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTcwOTMiLCJsZXZlbCI6InByb2Nlc3NpbmciLCJjb250cmFjdFZlcnNpb24iOiIxLjAuMCJ9fSwiaWF0IjoxNzQ4ODQ0OTE5fQ.aeb2uwwwEbaa3236XJhNOpJ_KxUIIefYeheAiw7OPtk_rXjmFOQ_aa7F09kEEgGK1NB3sijfVIEo5E96vMfZCQ", - "credential": { - "credentialSubject": [ - { - "claims": { - "id": "did:web:localhost%3A7093", - "contractVersion": "1.0.0", - "level": "processing" - } - } - ], - "id": "http://org.yourdataspace.com/credentials/1265", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": { - "id": "did:web:localhost%3A9876", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/local/provider/membership-credential.json b/deployment/assets/credentials/local/provider/membership-credential.json deleted file mode 100644 index 419beea63..000000000 --- a/deployment/assets/credentials/local/provider/membership-credential.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "id": "40e24588-b510-41ca-966c-c1e0f57d1b14", - "participantContextId": "did:web:localhost%3A7093", - "timestamp": 1700659822500, - "issuerId": "did:web:localhost%3A9876", - "holderId": "did:web:localhost%3A7093", - "state": 500, - "issuancePolicy": null, - "reissuancePolicy": null, - "verifiableCredential": { - "rawVc": "eyJraWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYja2V5LTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJhdWQiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJzdWIiOiJkaWQ6d2ViOnByb3ZpZGVyLWlkZW50aXR5aHViJTNBNzA4Mzpib2IiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvc3VpdGVzL2p3cy0yMDIwL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnL25zL2RpZC92MSIseyJtdmQtY3JlZGVudGlhbHMiOiJodHRwczovL3czaWQub3JnL212ZC9jcmVkZW50aWFscy8iLCJtZW1iZXJzaGlwIjoibXZkLWNyZWRlbnRpYWxzOm1lbWJlcnNoaXAiLCJtZW1iZXJzaGlwVHlwZSI6Im12ZC1jcmVkZW50aWFsczptZW1iZXJzaGlwVHlwZSIsIndlYnNpdGUiOiJtdmQtY3JlZGVudGlhbHM6d2Vic2l0ZSIsImNvbnRhY3QiOiJtdmQtY3JlZGVudGlhbHM6Y29udGFjdCIsInNpbmNlIjoibXZkLWNyZWRlbnRpYWxzOnNpbmNlIn1dLCJpZCI6Imh0dHA6Ly9vcmcueW91cmRhdGFzcGFjZS5jb20vY3JlZGVudGlhbHMvMTIzNCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJNZW1iZXJzaGlwQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTk4NzYiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA4LTE4VDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjpsb2NhbGhvc3QlM0E3MDkzIiwibWVtYmVyc2hpcCI6eyJtZW1iZXJzaGlwVHlwZSI6IlByb3NwZWN0TWVtYmVyIiwid2Vic2l0ZSI6Ind3dy5xdWl6enF1YXp6LmNvbSIsImNvbnRhY3QiOiJmb28uYmFyQHF1aXp6cXVhenouY29tIiwic2luY2UiOiIyMDIzLTAxLTAxVDAwOjAwOjAwWiJ9fX0sImlhdCI6MTc0ODg0NDkxOX0.HmC6-GC6GalGL6n8UQ2BNDOAS1qNJ0B6A7gObM_p0psOkZqCvtSQ-gwMTX8qd5gK7eihGuAEiMQ7Z_gCvgKKAw", - "format": "VC1_0_JWT", - "credential": { - "credentialSubject": [ - { - "claims": { - "membership": { - "contact": "fizz.buzz@quizzquazz.com", - "membershipType": "PartialMember", - "since": "2023-01-01T00:00:00Z", - "website": "www.quizzquazz.com" - } - }, - "id": "did:web:localhost%3A7093" - } - ], - "id": "http://org.yourdataspace.com/credentials/1234", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": { - "id": "did:web:localhost%3A9876", - "additionalProperties": {} - }, - "issuanceDate": 1702339200.000000000, - "expirationDate": null, - "credentialStatus": null, - "description": null, - "name": null - } - } -} diff --git a/deployment/assets/credentials/local/provider/unsigned/dataprocessor_vc.json b/deployment/assets/credentials/local/provider/unsigned/dataprocessor_vc.json deleted file mode 100644 index 3152b5f13..000000000 --- a/deployment/assets/credentials/local/provider/unsigned/dataprocessor_vc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "contractVersion": "mvd-credentials:contractVersion", - "level": "mvd-credentials:level" - } - ], - "id": "http://org.yourdataspace.com/credentials/2347", - "type": [ - "VerifiableCredential", - "DataProcessorCredential" - ], - "issuer": "did:web:localhost%3A9876", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:localhost%3A7093", - "level": "processing", - "contractVersion": "1.0.0" - } -} \ No newline at end of file diff --git a/deployment/assets/credentials/local/provider/unsigned/membership_vc.json b/deployment/assets/credentials/local/provider/unsigned/membership_vc.json deleted file mode 100644 index a2b9efe35..000000000 --- a/deployment/assets/credentials/local/provider/unsigned/membership_vc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/jws-2020/v1", - "https://www.w3.org/ns/did/v1", - { - "mvd-credentials": "https://w3id.org/mvd/credentials/", - "membership": "mvd-credentials:membership", - "membershipType": "mvd-credentials:membershipType", - "website": "mvd-credentials:website", - "contact": "mvd-credentials:contact", - "since": "mvd-credentials:since" - } - ], - "id": "http://org.yourdataspace.com/credentials/1234", - "type": [ - "VerifiableCredential", - "MembershipCredential" - ], - "issuer": "did:web:localhost%3A9876", - "issuanceDate": "2023-08-18T00:00:00Z", - "credentialSubject": { - "id": "did:web:localhost%3A7093", - "membership": { - "membershipType": "ProspectMember", - "website": "www.quizzquazz.com", - "contact": "foo.bar@quizzquazz.com", - "since": "2023-01-01T00:00:00Z" - } - } -} \ No newline at end of file diff --git a/deployment/assets/env/consumer_connector.env b/deployment/assets/env/consumer_connector.env deleted file mode 100644 index 78c803bf1..000000000 --- a/deployment/assets/env/consumer_connector.env +++ /dev/null @@ -1,38 +0,0 @@ -# control plane specific config -edc.iam.issuer.id=did:web:localhost%3A7083 -web.http.port=8080 -web.http.path=/api -web.http.management.port=8081 -web.http.management.path=/api/management/ -web.http.management.auth.type=tokenbased -web.http.management.auth.key=password -web.http.protocol.port=8082 -web.http.protocol.path=/api/dsp -web.http.control.port=8083 -web.http.control.path=/api/control -web.http.catalog.port=8084 -web.http.catalog.path=/api/catalog -web.http.catalog_auth.type=tokenbased -web.http.catalog_auth.key=password -web.http.version.port=8085 -web.http.version.path=/api/version -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=did:web:localhost%3A7083-alias -edc.iam.sts.publickey.id=did:web:localhost%3A7083#key-1 -edc.dsp.callback.address=http://localhost:8082/api/dsp -edc.participant.id=did:web:localhost%3A7083 -edc.catalog.cache.execution.delay.seconds=5 -edc.catalog.cache.execution.period.seconds=10 -edc.mvd.participants.list.file=deployment/assets/participants/participants.local.json -edc.management.context.enabled=true -edc.iam.sts.oauth.client.secret.alias=did:web:localhost%3A7083-sts-client-secret -edc.iam.sts.oauth.client.id=did:web:localhost%3A7083 -edc.iam.sts.oauth.token.url=http://localhost:7086/api/sts/token - -# dataplane specific config -edc.runtime.id=consumer-embedded-runtime -edc.transfer.proxy.token.verifier.publickey.alias=did:web:localhost%3A7083#key-1 -edc.transfer.proxy.token.signer.privatekey.alias=did:web:localhost%3A7083-alias -edc.dpf.selector.url=http://localhost:8083/api/control/v1/dataplanes -web.http.public.port=11001 -web.http.public.path=/api/public diff --git a/deployment/assets/env/consumer_identityhub.env b/deployment/assets/env/consumer_identityhub.env deleted file mode 100644 index 368423112..000000000 --- a/deployment/assets/env/consumer_identityhub.env +++ /dev/null @@ -1,19 +0,0 @@ -web.http.port=7080 -web.http.path=/api -web.http.credentials.port=7081 -web.http.credentials.path=/api/credentials -web.http.identity.port=7082 -web.http.identity.path=/api/identity -web.http.did.port=7083 -web.http.did.path=/ -web.http.version.port=7085 -web.http.version.path=/api/version -web.http.sts.port=7086 -web.http.sts.path=/api/sts -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=key-1 -edc.iam.sts.publickey.id=did:web:localhost%3A7083#key-1 -edc.ih.iam.publickey.path=./deployment/assets/consumer_public.pem -edc.ih.iam.id=did:web:localhost%3A7083 -edc.ih.api.superuser.key=c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo= -edc.mvd.credentials.path=deployment/assets/credentials/local/consumer/ \ No newline at end of file diff --git a/deployment/assets/env/issuerservice.env b/deployment/assets/env/issuerservice.env deleted file mode 100644 index e2aa1130d..000000000 --- a/deployment/assets/env/issuerservice.env +++ /dev/null @@ -1,15 +0,0 @@ -edc.issuer.statuslist.signing.key.alias=signing-key-alias -web.http.port=10010 -web.http.path=/api -web.http.sts.port=10011 -web.http.sts.path=/api/sts -web.http.issuance.port=10012 -web.http.issuance.path=/api/issuance -web.http.issueradmin.port=10013 -web.http.issueradmin.path=/api/admin -web.http.version.port=10014 -web.http.version.path=/.well-known/api -web.http.identity.port=10015 -web.http.identity.path=/api/identity -edc.iam.did.web.use.https=false -edc.ih.api.superuser.key=c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo= \ No newline at end of file diff --git a/deployment/assets/env/provider_catalogserver.env b/deployment/assets/env/provider_catalogserver.env deleted file mode 100644 index 7b9706ee6..000000000 --- a/deployment/assets/env/provider_catalogserver.env +++ /dev/null @@ -1,20 +0,0 @@ -edc.iam.issuer.id=did:web:localhost%3A7093 -web.http.port=8090 -web.http.path=/api -web.http.management.port=8091 -web.http.management.path=/api/management -web.http.management.auth.type=tokenbased -web.http.management.auth.key=password -web.http.protocol.port=8092 -web.http.protocol.path=/api/dsp -web.http.control.port=8093 -web.http.control.path=/api/control -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=did:web:localhost%3A7093-alias -edc.iam.sts.publickey.id=did:web:localhost%3A7093#key-1 -edc.dsp.callback.address=http://localhost:8092/api/dsp -edc.participant.id=did:web:localhost%3A7093 -edc.management.context.enabled=true -edc.iam.sts.oauth.client.secret.alias=did:web:localhost%3A7093-sts-client-secret -edc.iam.sts.oauth.client.id=did:web:localhost%3A7093 -edc.iam.sts.oauth.token.url=http://localhost:7096/api/sts/token \ No newline at end of file diff --git a/deployment/assets/env/provider_connector_manufacturing.env b/deployment/assets/env/provider_connector_manufacturing.env deleted file mode 100644 index e041c8fb0..000000000 --- a/deployment/assets/env/provider_connector_manufacturing.env +++ /dev/null @@ -1,38 +0,0 @@ -# control plane specific config -edc.iam.issuer.id=did:web:localhost%3A7093 -web.http.port=8290 -web.http.path=/api -web.http.management.port=8291 -web.http.management.path=/api/management/ -web.http.management.auth.type=tokenbased -web.http.management.auth.key=password -web.http.protocol.port=8292 -web.http.protocol.path=/api/dsp -web.http.control.port=8293 -web.http.control.path=/api/control -web.http.catalog.port=8294 -web.http.catalog.path=/api/catalog -web.http.catalog_auth.type=tokenbased -web.http.catalog_auth.key=password -web.http.version.port=8295 -web.http.version.path=/api/version -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=did:web:localhost%3A7093-alias -edc.iam.sts.publickey.id=did:web:localhost%3A7093#key-1 -edc.dsp.callback.address=http://localhost:8292/api/dsp -edc.participant.id=did:web:localhost%3A7093 -edc.catalog.cache.execution.delay.seconds=5 -edc.catalog.cache.execution.period.seconds=10 -edc.mvd.participants.list.file=deployment/assets/participants/participants.local.json -edc.management.context.enabled=true -edc.iam.sts.oauth.client.secret.alias=did:web:localhost%3A7093-sts-client-secret -edc.iam.sts.oauth.client.id=did:web:localhost%3A7093 -edc.iam.sts.oauth.token.url=http://localhost:7096/api/sts/token - -# dataplane specific config -edc.runtime.id=provider-manufacturing-embedded-runtime -edc.transfer.proxy.token.verifier.publickey.alias=did:web:localhost%3A7093#key-1 -edc.transfer.proxy.token.signer.privatekey.alias=did:web:localhost%3A7093-alias -edc.dpf.selector.url=http://localhost:8293/api/control/v1/dataplanes -web.http.public.port=12002 -web.http.public.path=/api/public diff --git a/deployment/assets/env/provider_connector_qna.env b/deployment/assets/env/provider_connector_qna.env deleted file mode 100644 index 353585982..000000000 --- a/deployment/assets/env/provider_connector_qna.env +++ /dev/null @@ -1,38 +0,0 @@ -# control plane specific config -edc.iam.issuer.id=did:web:localhost%3A7093 -web.http.port=8190 -web.http.path=/api -web.http.management.port=8191 -web.http.management.path=/api/management/ -web.http.management.auth.type=tokenbased -web.http.management.auth.key=password -web.http.protocol.port=8192 -web.http.protocol.path=/api/dsp -web.http.control.port=8193 -web.http.control.path=/api/control -web.http.catalog.port=8194 -web.http.catalog.path=/api/catalog -web.http.catalog_auth.type=tokenbased -web.http.catalog_auth.key=password -web.http.version.port=8195 -web.http.version.path=/api/version -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=did:web:localhost%3A7093-alias -edc.iam.sts.publickey.id=did:web:localhost%3A7093#key-1 -edc.dsp.callback.address=http://localhost:8192/api/dsp -edc.participant.id=did:web:localhost%3A7093 -edc.catalog.cache.execution.delay.seconds=5 -edc.catalog.cache.execution.period.seconds=10 -edc.mvd.participants.list.file=deployment/assets/participants/participants.local.json -edc.management.context.enabled=true -edc.iam.sts.oauth.client.secret.alias=did:web:localhost%3A7093-sts-client-secret -edc.iam.sts.oauth.client.id=did:web:localhost%3A7093 -edc.iam.sts.oauth.token.url=http://localhost:7096/api/sts/token - -# dataplane specific config -edc.runtime.id=provider-qna-embedded-runtime -edc.transfer.proxy.token.verifier.publickey.alias=did:web:localhost%3A7093#key-1 -edc.transfer.proxy.token.signer.privatekey.alias=did:web:localhost%3A7093-alias -edc.dpf.selector.url=http://localhost:8193/api/control/v1/dataplanes -web.http.public.port=12001 -web.http.public.path=/api/public diff --git a/deployment/assets/env/provider_identityhub.env b/deployment/assets/env/provider_identityhub.env deleted file mode 100644 index 7f2febbe9..000000000 --- a/deployment/assets/env/provider_identityhub.env +++ /dev/null @@ -1,19 +0,0 @@ -web.http.port=7090 -web.http.path=/api -web.http.credentials.port=7091 -web.http.credentials.path=/api/credentials/ -web.http.identity.port=7092 -web.http.identity.path=/api/identity -web.http.did.port=7093 -web.http.did.path=/ -web.http.version.port=7095 -web.http.version.path=/api/version -web.http.sts.port=7096 -web.http.sts.path=/api/sts -edc.iam.did.web.use.https=false -edc.iam.sts.privatekey.alias=key-1 -edc.iam.sts.publickey.id=did:web:localhost%3A7093#key-1 -edc.ih.iam.publickey.path=./deployment/assets/provider_public.pem -edc.ih.iam.id=did:web:localhost%3A7093 -edc.ih.api.superuser.key=c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo= -edc.mvd.credentials.path=deployment/assets/credentials/local/provider/ \ No newline at end of file diff --git a/deployment/assets/issuer/did.docker.json b/deployment/assets/issuer/did.docker.json deleted file mode 100644 index 1c819d142..000000000 --- a/deployment/assets/issuer/did.docker.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "service": [], - "verificationMethod": [ - { - "id": "did:web:localhost%3A9876#key-1", - "type": "JsonWebKey2020", - "controller": "did:web:localhost%3A9876", - "publicKeyMultibase": null, - "publicKeyJwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "Hsq2QXPbbsU7j6JwXstbpxGSgliI04g_fU3z2nwkuVc" - } - } - ], - "authentication": [ - "key-1" - ], - "id": "did:web:localhost%3A9876", - "@context": [ - "https://www.w3.org/ns/did/v1", - { - "@base": "did:web:localhost%3A9876" - } - ] -} \ No newline at end of file diff --git a/deployment/assets/issuer/did.k8s.json b/deployment/assets/issuer/did.k8s.json deleted file mode 100644 index b6b0d01dc..000000000 --- a/deployment/assets/issuer/did.k8s.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "service": [], - "verificationMethod": [ - { - "id": "did:web:dataspace-issuer#key-1", - "type": "JsonWebKey2020", - "controller": "did:web:dataspace-issuer", - "publicKeyMultibase": null, - "publicKeyJwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "Hsq2QXPbbsU7j6JwXstbpxGSgliI04g_fU3z2nwkuVc" - } - } - ], - "authentication": [ - "key-1" - ], - "id": "did:web:dataspace-issuer", - "@context": [ - "https://www.w3.org/ns/did/v1", - { - "@base": "did:web:dataspace-issuer" - } - ] -} \ No newline at end of file diff --git a/deployment/assets/issuer/nginx.conf b/deployment/assets/issuer/nginx.conf deleted file mode 100644 index fa45fbed2..000000000 --- a/deployment/assets/issuer/nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -events { worker_connections 1024; } - -http { - server { - listen 80; - root /var/www/; - index index.html; - } - } \ No newline at end of file diff --git a/deployment/assets/issuer_private.pem b/deployment/assets/issuer_private.pem deleted file mode 100644 index 8a63542f7..000000000 --- a/deployment/assets/issuer_private.pem +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEID1gMsekH7JN9Q/L2UMCBkAPET10NE0T2BB4c2rRSBzg ------END PRIVATE KEY----- diff --git a/deployment/assets/issuer_public.pem b/deployment/assets/issuer_public.pem deleted file mode 100644 index 51b250241..000000000 --- a/deployment/assets/issuer_public.pem +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAHsq2QXPbbsU7j6JwXstbpxGSgliI04g/fU3z2nwkuVc= ------END PUBLIC KEY----- diff --git a/deployment/assets/participants/participants.k8s.json b/deployment/assets/participants/participants.k8s.json deleted file mode 100644 index 993eeacee..000000000 --- a/deployment/assets/participants/participants.k8s.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "consumer-corp": "did:web:consumer-identityhub%3A7083:consumer", - "provider-corp": "did:web:provider-identityhub%3A7083:provider" -} \ No newline at end of file diff --git a/deployment/assets/participants/participants.local.json b/deployment/assets/participants/participants.local.json deleted file mode 100644 index ae3849cf4..000000000 --- a/deployment/assets/participants/participants.local.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "consumer-corp": "did:web:localhost%3A7083", - "provider-corp": "did:web:localhost%3A7093" -} \ No newline at end of file diff --git a/deployment/assets/provider_private.pem b/deployment/assets/provider_private.pem deleted file mode 100644 index 81c28bac2..000000000 --- a/deployment/assets/provider_private.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIARDUGJgKy1yzxkueIJ1k3MPUWQ/tbQWQNqW6TjyHpdcoAoGCCqGSM49 -AwEHoUQDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5roYnkAXuqCYfNK3ex+hMWFuiX -GUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== ------END EC PRIVATE KEY----- \ No newline at end of file diff --git a/deployment/assets/provider_public.pem b/deployment/assets/provider_public.pem deleted file mode 100644 index 977a19576..000000000 --- a/deployment/assets/provider_public.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5r -oYnkAXuqCYfNK3ex+hMWFuiXGUxHlzShAehR6wvwzV23bbC0tcFcVgW//A== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/deployment/consumer.tf b/deployment/consumer.tf deleted file mode 100644 index 2e194b89d..000000000 --- a/deployment/consumer.tf +++ /dev/null @@ -1,83 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -# This file deploys all the components needed for the consumer side of the scenario, -# i.e. the connector, an identityhub and a vault. - -# consumer connector -module "consumer-connector" { - source = "./modules/connector" - humanReadableName = "consumer" - participantId = var.consumer-did - database = { - user = "consumer" - password = "consumer" - url = "jdbc:postgresql://${module.consumer-postgres.database-url}/consumer" - } - vault-url = "http://consumer-vault:8200" - namespace = kubernetes_namespace.ns.metadata.0.name - sts-token-url = "${module.consumer-identityhub.sts-token-url}/token" - useSVE = var.useSVE -} - -# consumer identity hub -module "consumer-identityhub" { - depends_on = [module.consumer-vault] - source = "./modules/identity-hub" - credentials-dir = dirname("./assets/credentials/k8s/consumer/") - humanReadableName = "consumer-identityhub" - participantId = var.consumer-did - vault-url = "http://consumer-vault:8200" - service-name = "consumer" - database = { - user = "consumer" - password = "consumer" - url = "jdbc:postgresql://${module.consumer-postgres.database-url}/consumer" - } - namespace = kubernetes_namespace.ns.metadata.0.name - useSVE = var.useSVE -} - - -# consumer vault -module "consumer-vault" { - source = "./modules/vault" - humanReadableName = "consumer-vault" - namespace = kubernetes_namespace.ns.metadata.0.name -} - -# Postgres database for the consumer -module "consumer-postgres" { - depends_on = [kubernetes_config_map.postgres-initdb-config-consumer] - source = "./modules/postgres" - instance-name = "consumer" - init-sql-configs = ["consumer-initdb-config"] - namespace = kubernetes_namespace.ns.metadata.0.name -} - -# DB initialization for the EDC database -resource "kubernetes_config_map" "postgres-initdb-config-consumer" { - metadata { - name = "consumer-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "consumer-initdb-config.sql" = <<-EOT - CREATE USER consumer WITH ENCRYPTED PASSWORD 'consumer' SUPERUSER; - CREATE DATABASE consumer; - \c consumer consumer - - - EOT - } -} \ No newline at end of file diff --git a/deployment/issuer.tf b/deployment/issuer.tf deleted file mode 100644 index c49f658f3..000000000 --- a/deployment/issuer.tf +++ /dev/null @@ -1,68 +0,0 @@ -# -# Copyright (c) 2025 Cofinity-X -# -# 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: -# Cofinity-X - initial API and implementation -# - -module "dataspace-issuer" { - source = "./modules/issuer" - humanReadableName = "dataspace-issuer-service" - participantId = var.consumer-did - database = { - user = "issuer" - password = "issuer" - url = "jdbc:postgresql://${module.dataspace-issuer-postgres.database-url}/issuer" - } - vault-url = "http://consumer-vault:8200" - namespace = kubernetes_namespace.ns.metadata.0.name - useSVE = var.useSVE -} - -# Postgres database for the consumer -module "dataspace-issuer-postgres" { - depends_on = [kubernetes_config_map.issuer-initdb-config] - source = "./modules/postgres" - instance-name = "issuer" - init-sql-configs = ["issuer-initdb-config"] - namespace = kubernetes_namespace.ns.metadata.0.name -} - -# DB initialization for the EDC database -resource "kubernetes_config_map" "issuer-initdb-config" { - metadata { - name = "issuer-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "issuer-initdb-config.sql" = <<-EOT - CREATE USER issuer WITH ENCRYPTED PASSWORD 'issuer' SUPERUSER; - CREATE DATABASE issuer; - \c issuer issuer - - create table if not exists membership_attestations - ( - membership_type integer default 0, - holder_id varchar not null, - membership_start_date timestamp default now() not null, - id varchar default gen_random_uuid() not null - constraint attestations_pk - primary key - ); - - create unique index if not exists membership_attestation_holder_id_uindex - on membership_attestations (holder_id); - - -- seed the consumer and provider into the attestations DB, so that they can request FoobarCredentials sourcing - -- information from the database - INSERT INTO membership_attestations (membership_type, holder_id) VALUES (1, 'did:web:consumer-identityhub%3A7083:consumer'); - INSERT INTO membership_attestations (membership_type, holder_id) VALUES (2, 'did:web:provider-identityhub%3A7083:provider'); - EOT - } -} \ No newline at end of file diff --git a/deployment/issuer_nginx.tf b/deployment/issuer_nginx.tf deleted file mode 100644 index 3d6070a69..000000000 --- a/deployment/issuer_nginx.tf +++ /dev/null @@ -1,110 +0,0 @@ -# -# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) -# -# 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 -# - - -resource "kubernetes_deployment" "dataspace-issuer-did-server" { - metadata { - name = "dataspace-issuer-server" - namespace = kubernetes_namespace.ns.metadata.0.name - labels = { - App = "dataspace-issuer-server" - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = "dataspace-issuer-server" - } - } - - template { - metadata { - labels = { - App = "dataspace-issuer-server" - } - } - - spec { - - container { - image_pull_policy = "IfNotPresent" - image = "nginx:latest" - name = "nginx" - - port { - container_port = "80" - name = "web" - } - # maps the nginx.conf file - volume_mount { - mount_path = "/etc/nginx/nginx.conf" - sub_path = "nginx.conf" - name = "nginx-config" - } - - # this maps the did.json file such that it becomes available at htp:///dataspace-issuer/did.json - volume_mount { - mount_path = "/var/www/.well-known/did.json" - sub_path = "did.json" - name = "nginx-config" - } - } - - volume { - name = "nginx-config" - config_map { - name = "nginx-conf" - } - } - } - } - } -} - -resource "kubernetes_service" "dataspace-issuer-did-server-service" { - metadata { - name = "dataspace-issuer" # this must correlate with the Issuer's DID: did:web:dataspace-issuer -> http://dataspace-issuer/.well-known/did.json - namespace = kubernetes_namespace.ns.metadata.0.name - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.dataspace-issuer-did-server.spec.0.template.0.metadata[0].labels.App - } - # we need a stable IP, otherwise there will be a cycle with the issuer - port { - name = "web" - port = 80 - } - } -} - -resource "kubernetes_config_map" "nginx-map" { - metadata { - name = "nginx-conf" - namespace = kubernetes_namespace.ns.metadata.0.name - } - - data = { - "nginx.conf" = file("${path.root}/assets/issuer/nginx.conf") - "did.json" = file("${path.root}/assets/issuer/did.k8s.json") - } -} \ No newline at end of file diff --git a/deployment/kind.config.yaml b/deployment/kind.config.yaml deleted file mode 100644 index 9d918bb2d..000000000 --- a/deployment/kind.config.yaml +++ /dev/null @@ -1,37 +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 -# - ---- -kind: Cluster -apiVersion: kind.x-k8s.io/v1alpha4 -nodes: - - role: control-plane - kubeadmConfigPatches: - - | - kind: InitConfiguration - nodeRegistration: - kubeletExtraArgs: - node-labels: "ingress-ready=true" - extraPortMappings: - - containerPort: 80 - hostPort: 80 - protocol: TCP - - containerPort: 443 - hostPort: 443 - protocol: TCP \ No newline at end of file diff --git a/deployment/main.tf b/deployment/main.tf deleted file mode 100644 index 2a96bac99..000000000 --- a/deployment/main.tf +++ /dev/null @@ -1,51 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -terraform { - required_providers { - // for generating passwords, clientsecrets etc. - random = { - source = "hashicorp/random" - } - - kubernetes = { - source = "hashicorp/kubernetes" - } - helm = { - // used for Hashicorp Vault - source = "hashicorp/helm" - } - } -} - -provider "kubernetes" { - config_path = "~/.kube/config" -} - -provider "helm" { - kubernetes = { - config_path = "~/.kube/config" - } -} - -resource "kubernetes_namespace" "ns" { - metadata { - name = "mvd" - } -} diff --git a/deployment/modules/catalog-server/catalog-server.tf b/deployment/modules/catalog-server/catalog-server.tf deleted file mode 100644 index aad588755..000000000 --- a/deployment/modules/catalog-server/catalog-server.tf +++ /dev/null @@ -1,155 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_deployment" "connector" { - metadata { - name = lower(var.humanReadableName) - namespace = var.namespace - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = lower(var.humanReadableName) - } - } - - template { - metadata { - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - container { - name = lower(var.humanReadableName) - image = "catalog-server:latest" - image_pull_policy = "Never" - - env_from { - config_map_ref { - name = kubernetes_config_map.catalog-server-config.metadata[0].name - } - } - - port { - container_port = var.ports.management - name = "management-port" - } - port { - container_port = var.ports.web - name = "default-port" - } - port { - container_port = var.ports.debug - name = "debug-port" - } - - liveness_probe { - http_get { - path = "/api/check/liveness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - readiness_probe { - http_get { - path = "/api/check/readiness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - startup_probe { - http_get { - path = "/api/check/startup" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - volume_mount { - mount_path = "/etc/registry" - name = "registry-volume" - } - } - - volume { - name = "registry-volume" - config_map { - name = kubernetes_config_map.catalog-server-config.metadata[0].name - } - } - } - } - } -} - -resource "kubernetes_config_map" "catalog-server-config" { - metadata { - name = "${lower(var.humanReadableName)}-connector-config" - namespace = var.namespace - } - - ## Create databases for keycloak and MIW, create users and assign privileges - data = { - EDC_IAM_ISSUER_ID = var.participantId - EDC_IAM_DID_WEB_USE_HTTPS = false - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_MANAGEMENT_PORT = var.ports.management - WEB_HTTP_MANAGEMENT_PATH = "/api/management" - WEB_HTTP_MANAGEMENT_AUTH_TYPE = "tokenbased" - WEB_HTTP_MANAGEMENT_AUTH_KEY = "password" - WEB_HTTP_CONTROL_PORT = var.ports.control - WEB_HTTP_CONTROL_PATH = "/api/control" - WEB_HTTP_PROTOCOL_PORT = var.ports.protocol - WEB_HTTP_PROTOCOL_PATH = "/api/dsp" - EDC_DSP_CALLBACK_ADDRESS = "http://${local.controlplane-service-name}:${var.ports.protocol}/api/dsp" - EDC_IAM_STS_PRIVATEKEY_ALIAS = "${var.participantId}#${var.aliases.sts-private-key}" - EDC_IAM_STS_PUBLICKEY_ID = "${var.participantId}#${var.aliases.sts-public-key-id}" - JAVA_TOOL_OPTIONS = "${var.useSVE ? "-XX:UseSVE=0 " : ""}-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.debug}" - EDC_IH_AUDIENCE_REGISTRY_PATH = "/etc/registry/registry.json" - EDC_PARTICIPANT_ID = var.participantId - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - EDC_MVD_PARTICIPANTS_LIST_FILE = "/etc/participants/participants.json" - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - EDC_SQL_SCHEMA_AUTOCREATE = true - - # remote STS configuration - EDC_IAM_STS_OAUTH_TOKEN_URL = var.sts-token-url - EDC_IAM_STS_OAUTH_CLIENT_ID = var.participantId - EDC_IAM_STS_OAUTH_CLIENT_SECRET_ALIAS = "${var.participantId}-sts-client-secret" - } -} diff --git a/deployment/modules/catalog-server/ingress.tf b/deployment/modules/catalog-server/ingress.tf deleted file mode 100644 index ce34f33d1..000000000 --- a/deployment/modules/catalog-server/ingress.tf +++ /dev/null @@ -1,59 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_ingress_v1" "api-ingress" { - metadata { - name = "${var.humanReadableName}-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/$2" - "nginx.ingress.kubernetes.io/use-regex" = "true" - } - } - spec { - ingress_class_name = "nginx" - rule { - http { - path { - path = "/${var.humanReadableName}/health(/|$)(.*)" - backend { - service { - name = kubernetes_service.controlplane-service.metadata.0.name - port { - number = var.ports.web - } - } - } - } - - path { - path = "/${var.humanReadableName}/cp(/|$)(.*)" - backend { - service { - name = kubernetes_service.controlplane-service.metadata.0.name - port { - number = var.ports.management - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/deployment/modules/catalog-server/outputs.tf b/deployment/modules/catalog-server/outputs.tf deleted file mode 100644 index 0d47109a4..000000000 --- a/deployment/modules/catalog-server/outputs.tf +++ /dev/null @@ -1,37 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -output "connector-node-ip" { - value = kubernetes_service.controlplane-service.spec.0.cluster_ip -} - -output "ports" { - value = var.ports -} - -output "audience-mapping" { - value = { - # dspAudience = "http://${local.connector-cluster-ip}:${var.ports.protocol}/api/dsp" - dcpAudience = var.participantId - } -} - -output "ih-superuser-apikey" { - value = var.ih_superuser_apikey -} \ No newline at end of file diff --git a/deployment/modules/catalog-server/services.tf b/deployment/modules/catalog-server/services.tf deleted file mode 100644 index 781de4182..000000000 --- a/deployment/modules/catalog-server/services.tf +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_service" "controlplane-service" { - metadata { - name = local.controlplane-service-name - namespace = var.namespace - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.connector.spec.0.template.0.metadata[0].labels.App - } - port { - name = "health" - port = var.ports.web - } - port { - name = "management" - port = var.ports.management - } - port { - name = "protocol" - port = var.ports.protocol - } - port { - name = "debug" - port = var.ports.debug - } - } -} \ No newline at end of file diff --git a/deployment/modules/catalog-server/variables.tf b/deployment/modules/catalog-server/variables.tf deleted file mode 100644 index 02119125d..000000000 --- a/deployment/modules/catalog-server/variables.tf +++ /dev/null @@ -1,117 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -## Normally, you shouldn't need to change any values here. If you do, please be sure to also change them in the seed script (seed-k8s.sh). -## Neglecting to do that will render the connectors and identity hubs inoperable! - - -variable "image-pull-policy" { - default = "Always" - type = string - description = "Kubernetes ImagePullPolicy for all images" -} - -variable "humanReadableName" { - type = string - description = "Human readable name of the connector, NOT the ID!!. Required." -} - -variable "participantId" { - type = string - description = "DID:WEB identifier of the participant" -} - -variable "namespace" { - type = string -} - -variable "ports" { - type = object({ - web = number - management = number - protocol = number - control = number - debug = number - }) - default = { - web = 8080 - management = 8081 - protocol = 8082 - control = 8083 - debug = 1044 - } -} - -variable "database" { - type = object({ - url = string - user = string - password = string - }) -} - -variable "participant-list-file" { - type = string - default = "./assets/participants/participants.k8s.json" -} - -variable "ih_superuser_apikey" { - default = "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=" - description = "Management API Key for the Super-User. Defaults to 'base64(super-user).base64(super-secret-key)" - type = string -} - -variable "vault-token" { - default = "root" - description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" - type = string -} - -variable "vault-url" { - description = "URL of the Hashicorp Vault" - type = string -} - -variable "sts-token-url" { - description = "Full URL of the STS token endpoint" - type = string -} - -variable "aliases" { - type = object({ - sts-private-key = string - sts-public-key-id = string - }) - default = { - sts-private-key = "key-1" - sts-public-key-id = "key-1" - } -} - -variable "useSVE" { - type = bool - description = "If true, the -XX:UseSVE=0 switch (Scalable Vector Extensions) will be appended to the JAVA_TOOL_OPTIONS. Can help on macOs on Apple Silicon processors" - default = false -} - -locals { - name = lower(var.humanReadableName) - controlplane-service-name = "${var.humanReadableName}-controlplane" - ih-service-name = "${var.humanReadableName}-identityhub" -} diff --git a/deployment/modules/connector/controlplane.tf b/deployment/modules/connector/controlplane.tf deleted file mode 100644 index 86b7722b2..000000000 --- a/deployment/modules/connector/controlplane.tf +++ /dev/null @@ -1,185 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_deployment" "controlplane" { - metadata { - name = "${lower(var.humanReadableName)}-controlplane" - namespace = var.namespace - labels = { - App = "${lower(var.humanReadableName)}-controlplane" - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = "${lower(var.humanReadableName)}-controlplane" - } - } - - template { - metadata { - labels = { - App = "${lower(var.humanReadableName)}-controlplane" - } - } - - spec { - container { - name = "connector-${lower(var.humanReadableName)}" - image = "controlplane:latest" - image_pull_policy = "Never" - - env_from { - config_map_ref { - name = kubernetes_config_map.connector-config.metadata[0].name - } - } - - port { - container_port = var.ports.management - name = "management-port" - } - port { - container_port = var.ports.web - name = "default-port" - } - port { - container_port = var.ports.debug - name = "debug-port" - } - - liveness_probe { - http_get { - path = "/api/check/liveness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - readiness_probe { - http_get { - path = "/api/check/readiness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - startup_probe { - http_get { - path = "/api/check/startup" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - volume_mount { - mount_path = "/etc/registry" - name = "registry-volume" - } - - volume_mount { - mount_path = "/etc/participants" - name = "participants-volume" - } - } - - volume { - name = "registry-volume" - config_map { - name = kubernetes_config_map.connector-config.metadata[0].name - } - } - - volume { - name = "participants-volume" - config_map { - name = kubernetes_config_map.participants-map.metadata[0].name - } - } - } - } - } -} - -resource "kubernetes_config_map" "participants-map" { - metadata { - name = "${var.humanReadableName}-participants" - namespace = var.namespace - } - - data = { - "participants.json" = file(var.participant-list-file) - } - -} - -resource "kubernetes_config_map" "connector-config" { - metadata { - name = "${lower(var.humanReadableName)}-controlplane-config" - namespace = var.namespace - } - - ## Create databases for keycloak and MIW, create users and assign privileges - data = { - EDC_PARTICIPANT_ID = var.participantId - EDC_IAM_ISSUER_ID = var.participantId - EDC_IAM_DID_WEB_USE_HTTPS = false - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_MANAGEMENT_PORT = var.ports.management - WEB_HTTP_MANAGEMENT_PATH = "/api/management" - WEB_HTTP_MANAGEMENT_AUTH_TYPE = "tokenbased" - WEB_HTTP_MANAGEMENT_AUTH_KEY = "password" - WEB_HTTP_CONTROL_PORT = var.ports.control - WEB_HTTP_CONTROL_PATH = "/api/control" - WEB_HTTP_PROTOCOL_PORT = var.ports.protocol - WEB_HTTP_PROTOCOL_PATH = "/api/dsp" - WEB_HTTP_CATALOG_PORT = var.ports.catalog - WEB_HTTP_CATALOG_PATH = "/api/catalog" - WEB_HTTP_CATALOG_AUTH_TYPE = "tokenbased" - WEB_HTTP_CATALOG_AUTH_KEY = "password" - EDC_DSP_CALLBACK_ADDRESS = "http://${local.controlplane-service-name}:${var.ports.protocol}/api/dsp" - EDC_IAM_STS_PRIVATEKEY_ALIAS = "${var.participantId}#${var.aliases.sts-private-key}" - EDC_IAM_STS_PUBLICKEY_ID = "${var.participantId}#${var.aliases.sts-public-key-id}" - JAVA_TOOL_OPTIONS = "${var.useSVE ? "-XX:UseSVE=0 " : ""}-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.debug}" - EDC_IH_AUDIENCE_REGISTRY_PATH = "/etc/registry/registry.json" - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - EDC_MVD_PARTICIPANTS_LIST_FILE = "/etc/participants/participants.json" - EDC_CATALOG_CACHE_EXECUTION_DELAY_SECONDS = 10 - EDC_CATALOG_CACHE_EXECUTION_PERIOD_SECONDS = 10 - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - EDC_SQL_SCHEMA_AUTOCREATE = true - - # remote STS configuration - EDC_IAM_STS_OAUTH_TOKEN_URL = var.sts-token-url - EDC_IAM_STS_OAUTH_CLIENT_ID = var.participantId - EDC_IAM_STS_OAUTH_CLIENT_SECRET_ALIAS = "${var.participantId}-sts-client-secret" - } -} diff --git a/deployment/modules/connector/dataplane.tf b/deployment/modules/connector/dataplane.tf deleted file mode 100644 index 6a7d56e37..000000000 --- a/deployment/modules/connector/dataplane.tf +++ /dev/null @@ -1,137 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_deployment" "dataplane" { - # needs a hard dependency, otherwise the dataplane registration fails, and it is not retried - depends_on = [kubernetes_deployment.controlplane] - metadata { - name = "${lower(var.humanReadableName)}-dataplane" - namespace = var.namespace - labels = { - App = "${lower(var.humanReadableName)}-dataplane" - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = "${lower(var.humanReadableName)}-dataplane" - } - } - - template { - metadata { - labels = { - App = "${lower(var.humanReadableName)}-dataplane" - } - } - - spec { - container { - name = "dataplane-${lower(var.humanReadableName)}" - image = "dataplane:latest" - image_pull_policy = "Never" - - env_from { - config_map_ref { - name = kubernetes_config_map.dataplane-config.metadata[0].name - } - } - - port { - container_port = var.ports.public - name = "public-port" - } - - port { - container_port = var.ports.debug - name = "debug-port" - } - - liveness_probe { - http_get { - path = "/api/check/liveness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - readiness_probe { - http_get { - path = "/api/check/readiness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - startup_probe { - http_get { - path = "/api/check/startup" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - } - } - } - } -} - -resource "kubernetes_config_map" "dataplane-config" { - metadata { - name = "${lower(var.humanReadableName)}-dataplane-config" - namespace = var.namespace - } - - ## Create databases for keycloak and MIW, create users and assign privileges - data = { - # hostname is "localhost" by default, but must be the service name at which the dataplane is reachable. URL scheme and port are appended by the application - EDC_HOSTNAME = local.dataplane-service-name - EDC_RUNTIME_ID = "${var.humanReadableName}-dataplane" - EDC_PARTICIPANT_ID = var.participantId - EDC_TRANSFER_PROXY_TOKEN_VERIFIER_PUBLICKEY_ALIAS = "${var.participantId}#${var.aliases.sts-public-key-id}" - EDC_TRANSFER_PROXY_TOKEN_SIGNER_PRIVATEKEY_ALIAS = "${var.participantId}#${var.aliases.sts-private-key}" - EDC_DPF_SELECTOR_URL = "http://${local.controlplane-service-name}:${var.ports.control}/api/control/v1/dataplanes" - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_CONTROL_PORT = var.ports.control - WEB_HTTP_CONTROL_PATH = "/api/control" - WEB_HTTP_PUBLIC_PORT = var.ports.public - WEB_HTTP_PUBLIC_PATH = "/api/public" - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - JAVA_TOOL_OPTIONS = "${var.useSVE ? "-XX:UseSVE=0 " : ""}-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.debug}" - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - EDC_SQL_SCHEMA_AUTOCREATE = true - - # remote STS configuration - EDC_IAM_STS_OAUTH_TOKEN_URL = var.sts-token-url - EDC_IAM_STS_OAUTH_CLIENT_ID = var.participantId - EDC_IAM_STS_OAUTH_CLIENT_SECRET_ALIAS = "${var.participantId}-sts-client-secret" - } -} diff --git a/deployment/modules/connector/ingress.tf b/deployment/modules/connector/ingress.tf deleted file mode 100644 index 8c4b5d3af..000000000 --- a/deployment/modules/connector/ingress.tf +++ /dev/null @@ -1,99 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_ingress_v1" "api-ingress" { - metadata { - name = "${var.humanReadableName}-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/$2" - "nginx.ingress.kubernetes.io/use-regex" = "true" - } - } - spec { - ingress_class_name = "nginx" - rule { - http { - path { - path = "/${var.humanReadableName}/health(/|$)(.*)" - backend { - service { - name = kubernetes_service.controlplane-service.metadata.0.name - port { - number = var.ports.web - } - } - } - } - - path { - path = "/${var.humanReadableName}/cp(/|$)(.*)" - backend { - service { - name = kubernetes_service.controlplane-service.metadata.0.name - port { - number = var.ports.management - } - } - } - } - - path { - path = "/${var.humanReadableName}/public(/|$)(.*)" - backend { - service { - name = kubernetes_service.dataplane-service.metadata.0.name - port { - number = var.ports.public - } - } - } - } - - path { - path = "/${var.humanReadableName}/fc(/|$)(.*)" - backend { - service { - name = kubernetes_service.controlplane-service.metadata.0.name - port { - number = var.ports.catalog - } - } - } - } - - path { - path = "/${var.humanReadableName}/vault(/|$)(.*)" - backend { - service { - name = "${var.humanReadableName}-vault" - port { - number = 8200 - } - } - } - } - } - } - } -} - -locals { - data-plane-service = "${var.humanReadableName}-dataplane" -} diff --git a/deployment/modules/connector/outputs.tf b/deployment/modules/connector/outputs.tf deleted file mode 100644 index 81a5e9c54..000000000 --- a/deployment/modules/connector/outputs.tf +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -output "connector-node-ip" { - value = kubernetes_service.controlplane-service.spec.0.cluster_ip -} - - -output "database-name" { - value = var.database -} - -output "ports" { - value = var.ports -} - -output "audience-mapping" { - value = { - # dspAudience = "http://${local.connector-cluster-ip}:${var.ports.protocol}/api/dsp" - dcpAudience = var.participantId - } -} diff --git a/deployment/modules/connector/services.tf b/deployment/modules/connector/services.tf deleted file mode 100644 index 972e1cd2d..000000000 --- a/deployment/modules/connector/services.tf +++ /dev/null @@ -1,76 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_service" "controlplane-service" { - metadata { - name = local.controlplane-service-name - namespace = var.namespace - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.controlplane.spec.0.template.0.metadata[0].labels.App - } - port { - name = "health" - port = var.ports.web - } - port { - name = "management" - port = var.ports.management - } - port { - name = "catalog" - port = var.ports.catalog - } - port { - name = "protocol" - port = var.ports.protocol - } - port { - name = "debug" - port = var.ports.debug - } - port { - name = "control" - port = var.ports.control - } - } -} - -resource "kubernetes_service" "dataplane-service" { - metadata { - name = local.dataplane-service-name - namespace = var.namespace - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.dataplane.spec.0.template.0.metadata[0].labels.App - } - port { - name = "control" - port = var.ports.control - } - port { - name = "public" - port = var.ports.public - } - } -} \ No newline at end of file diff --git a/deployment/modules/connector/variables.tf b/deployment/modules/connector/variables.tf deleted file mode 100644 index 5c3c95f23..000000000 --- a/deployment/modules/connector/variables.tf +++ /dev/null @@ -1,115 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -## Normally, you shouldn't need to change any values here. If you do, please be sure to also change them in the seed script (seed-k8s.sh). -## Neglecting to do that will render the connectors and identity hubs inoperable! - - -variable "image-pull-policy" { - default = "Always" - type = string - description = "Kubernetes ImagePullPolicy for all images" -} - -variable "humanReadableName" { - type = string - description = "Human readable name of the connector, NOT the BPN!!. Required." -} - -variable "participantId" { - type = string - description = "DID:WEB identifier of the participant, will be used as runtime participantId" -} - -variable "namespace" { - type = string -} - -variable "ports" { - type = object({ - web = number - management = number - protocol = number - control = number - catalog = number - debug = number - public = number - }) - default = { - web = 8080 - management = 8081 - protocol = 8082 - control = 8083 - catalog = 8084 - debug = 1044 - public = 11002 - } -} - -variable "database" { - type = object({ - url = string - user = string - password = string - }) -} - -variable "participant-list-file" { - type = string - default = "./assets/participants/participants.k8s.json" -} - -variable "vault-token" { - default = "root" - description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" - type = string -} - -variable "vault-url" { - description = "URL of the Hashicorp Vault" - type = string -} - -variable "sts-token-url" { - description = "Full URL of the STS token endpoint" - type = string -} - -variable "aliases" { - type = object({ - sts-private-key = string - sts-public-key-id = string - }) - default = { - sts-private-key = "key-1" - sts-public-key-id = "key-1" - } -} - -variable "useSVE" { - type = bool - description = "If true, the -XX:UseSVE=0 switch (Scalable Vector Extensions) will be appended to the JAVA_TOOL_OPTIONS. Can help on macOs on Apple Silicon processors" - default = false -} - -locals { - name = lower(var.humanReadableName) - controlplane-service-name = "${var.humanReadableName}-controlplane" - dataplane-service-name = "${var.humanReadableName}-dataplane" -} diff --git a/deployment/modules/identity-hub/ingress.tf b/deployment/modules/identity-hub/ingress.tf deleted file mode 100644 index dfacfac2d..000000000 --- a/deployment/modules/identity-hub/ingress.tf +++ /dev/null @@ -1,81 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -resource "kubernetes_ingress_v1" "api-ingress" { - metadata { - name = "${var.humanReadableName}-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/$2" - "nginx.ingress.kubernetes.io/use-regex" = "true" - } - } - spec { - ingress_class_name = "nginx" - rule { - http { - - path { - path = "/${var.service-name}/cs(/|$)(.*)" - backend { - service { - name = kubernetes_service.ih-service.metadata.0.name - port { - number = var.ports.ih-identity-api - } - } - } - } - } - } - } -} - -// the DID endpoint can not actually modify the URL, otherwise it'll mess up the DID resolution -resource "kubernetes_ingress_v1" "did-ingress" { - metadata { - name = "${var.service-name}-did-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/${var.service-name}/$2" - } - } - - spec { - ingress_class_name = "nginx" - rule { - http { - - - # ingress routes for the DID endpoint - path { - path = "/${var.service-name}(/|&)(.*)" - backend { - service { - name = kubernetes_service.ih-service.metadata.0.name - port { - number = var.ports.ih-did - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/deployment/modules/identity-hub/main.tf b/deployment/modules/identity-hub/main.tf deleted file mode 100644 index e8aacd8f9..000000000 --- a/deployment/modules/identity-hub/main.tf +++ /dev/null @@ -1,171 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -resource "kubernetes_deployment" "identityhub" { - metadata { - name = lower(var.humanReadableName) - namespace = var.namespace - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = lower(var.humanReadableName) - } - } - - template { - metadata { - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - container { - image_pull_policy = "Never" - image = "identity-hub:latest" - name = "identity-hub" - - env_from { - config_map_ref { - name = kubernetes_config_map.identityhub-config.metadata[0].name - } - } - port { - container_port = var.ports.credentials-api - name = "creds-port" - } - - port { - container_port = var.ports.ih-debug - name = "debug" - } - port { - container_port = var.ports.ih-identity-api - name = "identity" - } - port { - container_port = var.ports.ih-did - name = "did" - } - port { - container_port = var.ports.web - name = "default-port" - } - - volume_mount { - mount_path = "/etc/credentials" - name = "credentials-volume" - } - - liveness_probe { - http_get { - path = "/api/check/liveness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - readiness_probe { - http_get { - path = "/api/check/readiness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - startup_probe { - http_get { - path = "/api/check/startup" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - } - - volume { - name = "credentials-volume" - config_map { - name = kubernetes_config_map.identityhub-credentials-map.metadata[0].name - } - } - } - - } - } -} - - -resource "kubernetes_config_map" "identityhub-credentials-map" { - metadata { - name = "${lower(var.humanReadableName)}-credentials" - namespace = var.namespace - } - - data = { - for f in fileset(var.credentials-dir, "*-credential.json") : f => file(join("/", [var.credentials-dir, f])) - } -} - -resource "kubernetes_config_map" "identityhub-config" { - metadata { - name = "${lower(var.humanReadableName)}-ih-config" - namespace = var.namespace - } - - data = { - # IdentityHub variables - EDC_IH_IAM_ID = var.participantId - EDC_IAM_DID_WEB_USE_HTTPS = false - EDC_IH_IAM_PUBLICKEY_ALIAS = local.public-key-alias - EDC_IH_API_SUPERUSER_KEY = var.ih_superuser_apikey - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_IDENTITY_PORT = var.ports.ih-identity-api - WEB_HTTP_IDENTITY_PATH = "/api/identity" - WEB_HTTP_IDENTITY_AUTH_KEY = "password" - WEB_HTTP_CREDENTIALS_PORT = var.ports.credentials-api - WEB_HTTP_CREDENTIALS_PATH = "/api/credentials" - WEB_HTTP_DID_PORT = var.ports.ih-did - WEB_HTTP_DID_PATH = "/" - WEB_HTTP_STS_PORT = var.ports.sts-api - WEB_HTTP_STS_PATH = var.sts-token-path - JAVA_TOOL_OPTIONS = "${var.useSVE ? "-XX:UseSVE=0 " : ""}-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.debug}" - EDC_IAM_STS_PRIVATEKEY_ALIAS = var.aliases.sts-private-key - EDC_IAM_STS_PUBLICKEY_ID = var.aliases.sts-public-key-id - EDC_MVD_CREDENTIALS_PATH = "/etc/credentials/" - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - EDC_SQL_SCHEMA_AUTOCREATE = true - EDC_IAM_ACCESSTOKEN_JTI_VALIDATION = true - - } -} - -locals { - public-key-alias = "${var.humanReadableName}-publickey" -} \ No newline at end of file diff --git a/deployment/modules/identity-hub/outputs.tf b/deployment/modules/identity-hub/outputs.tf deleted file mode 100644 index 007553071..000000000 --- a/deployment/modules/identity-hub/outputs.tf +++ /dev/null @@ -1,42 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -output "identity-hub-node-ip" { - value = kubernetes_service.ih-service.spec.0.cluster_ip -} - - -output "ports" { - value = var.ports -} - -output "ih-superuser-apikey" { - value = var.ih_superuser_apikey -} - -output "credentials" { - value = { - path = var.credentials-dir - content = fileset(var.credentials-dir, "*-credential.json") - } -} - -output "sts-token-url" { - value = "http://${kubernetes_service.ih-service.metadata.0.name}:${var.ports.sts-api}${var.sts-token-path}" -} \ No newline at end of file diff --git a/deployment/modules/identity-hub/services.tf b/deployment/modules/identity-hub/services.tf deleted file mode 100644 index 65b503930..000000000 --- a/deployment/modules/identity-hub/services.tf +++ /dev/null @@ -1,46 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -resource "kubernetes_service" "ih-service" { - metadata { - name = var.humanReadableName - namespace = var.namespace - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.identityhub.spec.0.template.0.metadata[0].labels.App - } - # we need a stable IP, otherwise there will be a cycle with the issuer - port { - name = "credentials" - port = var.ports.credentials-api - } - port { - name = "debug" - port = var.ports.ih-debug - } - port { - name = "management" - port = var.ports.ih-identity-api - } - port { - name = "did" - port = var.ports.ih-did - } - port { - name = "sts" - port = var.ports.sts-api - } - } -} \ No newline at end of file diff --git a/deployment/modules/identity-hub/variables.tf b/deployment/modules/identity-hub/variables.tf deleted file mode 100644 index c8e0f0d65..000000000 --- a/deployment/modules/identity-hub/variables.tf +++ /dev/null @@ -1,115 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -## Normally, you shouldn't need to change any values here. If you do, please be sure to also change them in the seed script (seed-k8s.sh). -## Neglecting to do that will render the connectors and identity hubs inoperable! - - -variable "humanReadableName" { - type = string - description = "Human readable name of the connector, NOT the ID!!. Required." -} - -variable "participantId" { - type = string - description = "Participant ID of the connector. Usually a DID" -} - -variable "namespace" { - type = string -} - -variable "ports" { - type = object({ - web = number - debug = number - ih-debug = number - ih-did = number - ih-identity-api = number - credentials-api = number - sts-api = number - }) - default = { - web = 7080 - debug = 1044 - ih-debug = 1044 - ih-did = 7083 - ih-identity-api = 7081 - credentials-api = 7082 - sts-api = 7084 - } -} - -variable "credentials-dir" { - type = string - description = "JSON object containing the credentials to seed, sorted by human-readable participant name" -} - -variable "ih_superuser_apikey" { - default = "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=" - description = "Management API Key for the Super-User. Defaults to 'base64(super-user).base64(super-secret-key)" - type = string -} - -variable "vault-url" { - description = "URL of the Hashicorp Vault" - type = string -} - -variable "vault-token" { - default = "root" - description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" - type = string -} - -variable "aliases" { - type = object({ - sts-private-key = string - sts-public-key-id = string - }) - default = { - sts-private-key = "key-1" - sts-public-key-id = "key-1" - } -} - -variable "service-name" { - type = string - description = "Name of the Service endpoint" -} - -variable "database" { - type = object({ - url = string - user = string - password = string - }) -} - -variable "useSVE" { - type = bool - description = "If true, the -XX:UseSVE=0 switch (Scalable Vector Extensions) will be appended to the JAVA_TOOL_OPTIONS. Can help on macOs on Apple Silicon processors" - default = false -} - -variable "sts-token-path" { - description = "path suffix of the STS token API" - type = string - default = "/api/sts" -} \ No newline at end of file diff --git a/deployment/modules/issuer/ingress.tf b/deployment/modules/issuer/ingress.tf deleted file mode 100644 index b1219dd67..000000000 --- a/deployment/modules/issuer/ingress.tf +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright (c) 2025 Cofinity-X -# -# 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: -# Cofinity-X - initial API and implementation -# - -resource "kubernetes_ingress_v1" "api-ingress" { - metadata { - name = "${var.humanReadableName}-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/$2" - "nginx.ingress.kubernetes.io/use-regex" = "true" - } - } - spec { - ingress_class_name = "nginx" - rule { - http { - - path { - path = "/issuer/cs(/|$)(.*)" - backend { - service { - name = kubernetes_service.issuerservice-service.metadata.0.name - port { - number = var.ports.identity - } - } - } - } - - path { - path = "/issuer/ad(/|$)(.*)" - backend { - service { - name = kubernetes_service.issuerservice-service.metadata.0.name - port { - number = var.ports.issueradmin - } - } - } - } - } - } - } -} - -// the DID endpoint can not actually modify the URL, otherwise it'll mess up the DID resolution -resource "kubernetes_ingress_v1" "did-ingress" { - metadata { - name = "${var.humanReadableName}-did-ingress" - namespace = var.namespace - annotations = { - "nginx.ingress.kubernetes.io/rewrite-target" = "/issuer/$2" - } - } - - spec { - ingress_class_name = "nginx" - rule { - http { - - - # ingress routes for the DID endpoint - path { - path = "/issuer(/|&)(.*)" - backend { - service { - name = kubernetes_service.issuerservice-service.metadata.0.name - port { - number = var.ports.did - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/deployment/modules/issuer/main.tf b/deployment/modules/issuer/main.tf deleted file mode 100644 index c19adf7e0..000000000 --- a/deployment/modules/issuer/main.tf +++ /dev/null @@ -1,159 +0,0 @@ -# -# Copyright (c) 2025 Cofinity-X -# -# 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: -# Cofinity-X - initial API and implementation -# - -resource "kubernetes_deployment" "issuerservice" { - metadata { - name = lower(var.humanReadableName) - namespace = var.namespace - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = lower(var.humanReadableName) - } - } - - template { - metadata { - labels = { - App = lower(var.humanReadableName) - } - } - - spec { - container { - image_pull_policy = "Never" - image = "issuerservice:latest" - name = "issuerservice" - - env_from { - config_map_ref { - name = kubernetes_config_map.issuerservice-config.metadata[0].name - } - } - port { - container_port = var.ports.web - name = "web" - } - - port { - container_port = var.ports.sts - name = "sts" - } - port { - container_port = var.ports.debug - name = "debug" - } - port { - container_port = var.ports.issuance - name = "issuance" - } - port { - container_port = var.ports.issueradmin - name = "issueradmin" - } - port { - container_port = var.ports.identity - name = "identity-port" - } - - port { - container_port = var.ports.did - name = "did" - } - - liveness_probe { - http_get { - path = "/api/check/liveness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - readiness_probe { - http_get { - path = "/api/check/readiness" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - - startup_probe { - http_get { - path = "/api/check/startup" - port = var.ports.web - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - } - } - } - } -} - -resource "kubernetes_config_map" "issuerservice-config" { - metadata { - name = "${lower(var.humanReadableName)}-config" - namespace = var.namespace - } - - data = { - EDC_ISSUER_STATUSLIST_SIGNING_KEY_ALIAS = "statuslist-signing-key" - EDC_IH_API_SUPERUSER_KEY = var.superuser_apikey - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_STS_PORT = var.ports.sts - WEB_HTTP_STS_PATH = "/api/sts" - WEB_HTTP_ISSUANCE_PORT = var.ports.issuance - WEB_HTTP_ISSUANCE_PATH = "/api/issuance" - WEB_HTTP_ISSUERADMIN_PORT = var.ports.issueradmin - WEB_HTTP_ISSUERADMIN_PATH = "/api/admin" - WEB_HTTP_VERSION_PORT = var.ports.version - WEB_HTTP_VERSION_PATH = "/.well-known/api" - WEB_HTTP_IDENTITY_PORT = var.ports.identity - WEB_HTTP_IDENTITY_PATH = "/api/identity" - WEB_HTTP_IDENTITY_AUTH_KEY = "password" - WEB_HTTP_DID_PORT = var.ports.did - WEB_HTTP_DID_PATH = "/" - - JAVA_TOOL_OPTIONS = "${var.useSVE ? "-XX:UseSVE=0 " : ""}-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.debug}" - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - - # even though we have a default data source, we need a named datasource for the DatabaseAttestationSource, because - # that is configured in the AttestationDefinition - EDC_DATASOURCE_MEMBERSHIP_URL = var.database.url - EDC_DATASOURCE_MEMBERSHIP_USER = var.database.user - EDC_DATASOURCE_MEMBERSHIP_PASSWORD = var.database.password - - EDC_SQL_SCHEMA_AUTOCREATE = true - EDC_IAM_ACCESSTOKEN_JTI_VALIDATION = true - EDC_IAM_DID_WEB_USE_HTTPS = false - - } -} - diff --git a/deployment/modules/issuer/services.tf b/deployment/modules/issuer/services.tf deleted file mode 100644 index 8f577e265..000000000 --- a/deployment/modules/issuer/services.tf +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright (c) 2025 Cofinity-X -# -# 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: -# Cofinity-X - initial API and implementation -# - -resource "kubernetes_service" "issuerservice-service" { - metadata { - name = var.humanReadableName - namespace = var.namespace - } - spec { - type = "NodePort" - selector = { - App = kubernetes_deployment.issuerservice.spec.0.template.0.metadata[0].labels.App - } - port { - name = "web" - port = var.ports.web - } - port { - name = "sts" - port = var.ports.sts - } - port { - name = "debug" - port = var.ports.debug - } - port { - name = "issuance" - port = var.ports.issuance - } - port { - name = "issueradmin" - port = var.ports.issueradmin - } - port { - name = "identity" - port = var.ports.identity - } - port { - name = "did" - port = var.ports.did - } - } -} \ No newline at end of file diff --git a/deployment/modules/issuer/variables.tf b/deployment/modules/issuer/variables.tf deleted file mode 100644 index c2119289a..000000000 --- a/deployment/modules/issuer/variables.tf +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (c) 2025 Cofinity-X -# -# 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: -# Cofinity-X - initial API and implementation -# - - -variable "humanReadableName" { - type = string - description = "Human readable name of the issuer, NOT the ID!!. Required." - default = "issuerservice" -} - -variable "participantId" { - type = string - description = "Participant ID of the issuer. Usually a DID" -} - -variable "namespace" { - type = string -} - -variable "ports" { - type = object({ - web = number - sts = number - issuance = number - issueradmin = number - version = number - identity = number - debug = number - did = number - }) - default = { - web = 10010 - sts = 10011 - issuance = 10012 - issueradmin = 10013 - version = 10014 - identity = 10015 - did = 10016 - debug = 1044 - } -} - -variable "database" { - type = object({ - url = string - user = string - password = string - }) -} - -variable "useSVE" { - type = bool - description = "If true, the -XX:UseSVE=0 switch (Scalable Vector Extensions) will be appended to the JAVA_TOOL_OPTIONS. Can help on macOs on Apple Silicon processors" - default = false -} - -variable "vault-url" { - description = "URL of the Hashicorp Vault" - type = string -} - -variable "vault-token" { - default = "root" - description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" - type = string -} - -variable "superuser_apikey" { - default = "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=" - description = "Management API Key for the Super-User. Defaults to 'base64(super-user).base64(super-secret-key)" - type = string -} diff --git a/deployment/modules/postgres/main.tf b/deployment/modules/postgres/main.tf deleted file mode 100644 index 31571f954..000000000 --- a/deployment/modules/postgres/main.tf +++ /dev/null @@ -1,131 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -resource "kubernetes_deployment" "postgres" { - metadata { - name = local.app-name - namespace = var.namespace - labels = { - App = local.app-name - } - } - - spec { - replicas = 1 - selector { - match_labels = { - App = local.app-name - } - } - template { - metadata { - labels = { - App = local.app-name - } - } - spec { - container { - image = local.pg-image - name = local.app-name - - env_from { - config_map_ref { - name = kubernetes_config_map.postgres-env.metadata[0].name - } - } - port { - container_port = 5432 - name = "postgres-port" - } - - dynamic "volume_mount" { - for_each = toset(var.init-sql-configs) - content { - mount_path = "/docker-entrypoint-initdb.d/${volume_mount.value}.sql" - name = volume_mount.value - sub_path = "${volume_mount.value}.sql" - read_only = true - } - } - - # Uncomment this to assign (more) resources - # resources { - # limits = { - # cpu = "2" - # memory = "512Mi" - # } - # requests = { - # cpu = "250m" - # memory = "50Mi" - # } - # } - liveness_probe { - exec { - command = ["pg_isready", "-U", "postgres"] - } - failure_threshold = 10 - period_seconds = 5 - timeout_seconds = 30 - } - } - - dynamic "volume" { - for_each = toset(var.init-sql-configs) - content { - name = volume.value - config_map { - name = volume.value - } - } - } - } - } - } -} - -resource "kubernetes_config_map" "postgres-env" { - metadata { - name = "${local.app-name}-env" - namespace = var.namespace - } - - ## Create databases for keycloak and MIW, create users and assign privileges - data = { - POSTGRES_USER = "postgres" - POSTGRES_PASSWORD = "postgres" - } -} - -resource "kubernetes_service" "pg-service" { - metadata { - name = "${local.app-name}-service" - namespace = var.namespace - } - spec { - selector = { - App = kubernetes_deployment.postgres.spec.0.template.0.metadata[0].labels.App - } - port { - name = "pg-port" - port = var.database-port - target_port = var.database-port - } - } -} - -locals { - app-name = "${var.instance-name}-postgres" - pg-image = "postgres:16.3-alpine3.20" - db-ip = kubernetes_service.pg-service.spec.0.cluster_ip - db-url = "${kubernetes_service.pg-service.metadata[0].name}:${var.database-port}" -} diff --git a/deployment/modules/postgres/outputs.tf b/deployment/modules/postgres/outputs.tf deleted file mode 100644 index 5e1ddfb1d..000000000 --- a/deployment/modules/postgres/outputs.tf +++ /dev/null @@ -1,28 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -output "instance-name" { - value = var.instance-name -} - -output "database-host" { - value = local.db-ip -} - -output "database-port" { - value = var.database-port -} - -output "database-url" { - value = local.db-url -} diff --git a/deployment/modules/postgres/variables.tf b/deployment/modules/postgres/variables.tf deleted file mode 100644 index 7c5e66be2..000000000 --- a/deployment/modules/postgres/variables.tf +++ /dev/null @@ -1,29 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -variable "instance-name" { - description = "Name for the Postgres instance, must be unique for each postgres instances" -} - -variable "database-port" { - default = 5432 -} - -variable "init-sql-configs" { - description = "Name of config maps with init sql scripts" - default = [] -} - -variable "namespace" { - description = "kubernetes namespace where the PG instance is deployed" -} \ No newline at end of file diff --git a/deployment/modules/vault/variables.tf b/deployment/modules/vault/variables.tf deleted file mode 100644 index 1ad67483a..000000000 --- a/deployment/modules/vault/variables.tf +++ /dev/null @@ -1,44 +0,0 @@ -# -# Copyright (c) 2024 Contributors to the Eclipse Foundation -# -# 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 -# - -variable "humanReadableName" { - type = string - description = "Human readable name. Should not contain special characters" -} - -variable "namespace" { - type = string -} - -variable "vault-token" { - default = "root" - description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" - type = string -} - -variable "aliases" { - type = object({ - sts-private-key = string - sts-public-key-id = string - }) - default = { - sts-private-key = "key-1" - sts-public-key-id = "key-1" - } -} \ No newline at end of file diff --git a/deployment/modules/vault/vault-values.yaml b/deployment/modules/vault/vault-values.yaml deleted file mode 100644 index e7a5167ed..000000000 --- a/deployment/modules/vault/vault-values.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -server: - postStart: -hashicorp: - timeout: 30 - healthCheck: - enabled: true - standbyOk: true - paths: - secret: /v1/secret diff --git a/deployment/modules/vault/vault.tf b/deployment/modules/vault/vault.tf deleted file mode 100644 index 371ccf964..000000000 --- a/deployment/modules/vault/vault.tf +++ /dev/null @@ -1,70 +0,0 @@ -# -# Copyright (c) 2024 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 -# - -resource "helm_release" "vault" { - name = var.humanReadableName - namespace = var.namespace - - force_update = true - dependency_update = true - reuse_values = true - cleanup_on_fail = true - replace = true - - repository = "https://helm.releases.hashicorp.com" - chart = "vault" - - - - set = [ - { - name = "server.dev.devRootToken" - value = var.vault-token - }, - { - name = "server.dev.enabled" - value = true - }, - { - name = "injector.enabled" - value = false - }, - { - name = "hashicorp.token" - value = var.vault-token - } - ] - - values = [ - file("${path.module}/vault-values.yaml"), - # yamlencode({ - # "server" : { - # "postStart" : [ - # "sh", - # "-c", - # join(" && ", [ - # "sleep 5", - # "/bin/vault kv put secret/${var.aliases.sts-private-key} content=\"${tls_private_key.private_signing_key.private_key_pem}\"", - # # "/bin/vault kv put secret/${local.public-key-alias} content=\"${tls_private_key.ecdsa.public_key_pem}\"" - # ]) - # ] - # } - # }), - ] -} -# -# ECDSA key with P256 elliptic curve -resource "tls_private_key" "private_signing_key" { - algorithm = "ECDSA" - ecdsa_curve = "P256" -} diff --git a/deployment/outputs.tf b/deployment/outputs.tf deleted file mode 100644 index 6d848a95f..000000000 --- a/deployment/outputs.tf +++ /dev/null @@ -1,31 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -output "consumer-jdbc-url" { - # jdbc:postgresql://localhost:5432/mydatabase?currentSchema=myschema - value = "jdbc:postgresql://${module.consumer-postgres.database-url}/consumer" -} - -output "provider-jdbc-url" { - value = { - catalog-server = "jdbc:postgresql://${module.provider-postgres.database-url}/catalog_server" - provider-qna = "jdbc:postgresql://${module.provider-postgres.database-url}/provider_qna" - provider-manufacturing = "jdbc:postgresql://${module.provider-postgres.database-url}/provider_manufacturing" - } -} \ No newline at end of file diff --git a/deployment/postman/MVD K8S.postman_environment.json b/deployment/postman/MVD K8S.postman_environment.json deleted file mode 100644 index 614bb5e74..000000000 --- a/deployment/postman/MVD K8S.postman_environment.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "id": "9330e6b5-fffa-40a9-8835-5e76233f9ccd", - "name": "MVD K8S", - "values": [ - { - "key": "HOST", - "value": "http://localhost/consumer/cp", - "type": "default", - "enabled": true - }, - { - "key": "CS_URL", - "value": "http://localhost/consumer/cs/", - "type": "default", - "enabled": true - }, - { - "key": "CATALOG_SERVER_DSP_URL", - "value": "http://provider-catalog-server-controlplane:8082", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_CATALOG_QUERY_URL", - "value": "http://localhost/consumer/fc", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_PUBLIC_API", - "value": "http://localhost/provider-qna/public", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_DSP_URL", - "value": "http://provider-qna-controlplane:8082", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_ID", - "value": "did:web:provider-identityhub%3A7083:provider", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_NAME", - "value": "MVD Provider Participant", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_ID", - "value": "did:web:consumer-identityhub%3A7083:consumer", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_NAME", - "value": "MVD Consumer Participant", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_DID", - "value": "did:web:dataspace-issuer-service%3A10016:issuer", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_BASE_URL", - "value": "http://localhost/issuer/ad", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_ADMIN_URL", - "value": "", - "type": "default", - "enabled": true - }, - { - "key": "PARTICIPANT_ID_BASE64", - "value": "", - "type": "default", - "enabled": true - }, - { - "key": "REQUEST_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "POLICY_ID_ASSET_1", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "CONTRACT_NEGOTIATION_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "CONTRACT_AGREEMENT_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "TRANSFER_PROCESS_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AUTHORIZATION", - "value": "", - "type": "any", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2025-03-05T06:26:50.206Z", - "_postman_exported_using": "Postman/11.34.4" -} \ No newline at end of file diff --git a/deployment/postman/MVD Local Development.postman_environment.json b/deployment/postman/MVD Local Development.postman_environment.json deleted file mode 100644 index bc81584fc..000000000 --- a/deployment/postman/MVD Local Development.postman_environment.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "id": "448d5e51-e4f9-4b2a-96ea-1054aab52c1d", - "name": "MVD Local Development", - "values": [ - { - "key": "HOST", - "value": "http://localhost:8081", - "type": "default", - "enabled": true - }, - { - "key": "CS_URL", - "value": "http://localhost:7082", - "type": "default", - "enabled": true - }, - { - "key": "CATALOG_SERVER_DSP_URL", - "value": "http://localhost:8092", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_CATALOG_QUERY_URL", - "value": "http://localhost:8084", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_DSP_URL", - "value": "http://localhost:8192", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_PUBLIC_API", - "value": "http://localhost:12001", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_BASE_URL", - "value": "", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_ADMIN_URL", - "value": "http://localhost:10013", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_ID", - "value": "did:web:localhost%3A7083", - "type": "default", - "enabled": true - }, - { - "key": "CONSUMER_NAME", - "value": "MVD Consumer Participant", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_ID", - "value": "did:web:localhost%3A7093", - "type": "default", - "enabled": true - }, - { - "key": "PROVIDER_NAME", - "value": "MVD Provider Participant", - "type": "default", - "enabled": true - }, - { - "key": "ISSUER_DID", - "value": "did:web:localhost%3A10100", - "type": "default", - "enabled": true - }, - { - "key": "POLICY_ID_ASSET_1", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PARTICIPANT_ID_BASE64", - "value": "ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgz", - "type": "default", - "enabled": true - }, - { - "key": "REQUEST_ID", - "value": "", - "type": "any", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2025-03-05T06:26:44.509Z", - "_postman_exported_using": "Postman/11.34.4" -} \ No newline at end of file diff --git a/deployment/postman/MVD.postman_collection.json b/deployment/postman/MVD.postman_collection.json deleted file mode 100644 index c373dd48b..000000000 --- a/deployment/postman/MVD.postman_collection.json +++ /dev/null @@ -1,6218 +0,0 @@ -{ - "info": { - "_postman_id": "46b6d41a-f249-4571-8ffe-0ee7fc25f6e0", - "name": "MVD", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "647585" - }, - "item": [ - { - "name": "Seed", - "item": [ - { - "name": "Create Asset 1", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"asset-1\",\n \"@type\": \"Asset\",\n \"properties\": {\n \"description\": \"This asset requires Membership to view and negotiate.\"\n },\n \"dataAddress\": {\n \"@type\": \"DataAddress\",\n \"type\": \"HttpData\",\n \"baseUrl\": \"https://jsonplaceholder.typicode.com/todos\",\n \"proxyPath\": \"true\",\n \"proxyQueryParams\": \"true\"\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "Create Asset 2", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"asset-2\",\n \"@type\": \"Asset\",\n \"properties\": {\n \"description\": \"This asset requires Membership to view and SensitiveData credential to negotiate.\"\n },\n \"dataAddress\": {\n \"@type\": \"DataAddress\",\n \"type\": \"HttpData\",\n \"baseUrl\": \"https://jsonplaceholder.typicode.com/todos\",\n \"proxyPath\": \"true\",\n \"proxyQueryParams\": \"true\"\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "Create Membership Policy", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"PolicyDefinition\",\n \"@id\": \"require-membership\",\n \"policy\": {\n \"@type\": \"Set\",\n \"permission\": [\n {\n \"action\": \"use\",\n \"constraint\": {\n \"leftOperand\": \"MembershipCredential\",\n \"operator\": \"eq\",\n \"rightOperand\": \"active\"\n }\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/policydefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "Create DataProcessor policy", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"PolicyDefinition\",\n \"@id\": \"require-dataprocessor\",\n \"policy\": {\n \"@type\": \"Set\",\n \"obligation\": [\n {\n \"action\": \"use\",\n \"constraint\": {\n \"leftOperand\": \"DataAccess.level\",\n \"operator\": \"eq\",\n \"rightOperand\": \"processing\"\n }\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/policydefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "Create Sensitive Data Processor policy", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"PolicyDefinition\",\n \"@id\": \"require-sensitive\",\n \"policy\": {\n \"@type\": \"Set\",\n \"obligation\": [\n {\n \"action\": \"use\",\n \"constraint\": {\n \"leftOperand\": \"DataAccess.level\",\n \"operator\": \"eq\",\n \"rightOperand\": \"sensitive\"\n }\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/policydefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "Create \"member-and-data-cred\" definition", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"member-and-dataprocessor-def\",\n \"@type\": \"ContractDefinition\",\n \"accessPolicyId\": \"require-membership\",\n \"contractPolicyId\": \"require-dataprocessor\",\n \"assetsSelector\": {\n \"@type\": \"Criterion\",\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"operator\": \"=\",\n \"operandRight\": \"asset-1\"\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/contractdefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "contractdefinitions" - ] - } - }, - "response": [] - }, - { - "name": "Create \"require sensitive\" definition", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"sensitive-only-def\",\n \"@type\": \"ContractDefinition\",\n \"accessPolicyId\": \"require-membership\",\n \"contractPolicyId\": \"require-sensitive\",\n \"assetsSelector\": {\n \"@type\": \"Criterion\",\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"operator\": \"=\",\n \"operandRight\": \"asset-2\"\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/contractdefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "contractdefinitions" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204, 409])", - "})" - ] - } - } - ] - }, - { - "name": "Seed Catalog Server", - "item": [ - { - "name": "Create linked Asset for provider-qna", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"linked-asset-provider-qna\",\n \"@type\": \"CatalogAsset\",\n \"properties\": {\n \"description\": \"This is a linked asset that points to the catalog of the provider's Q&A department.\",\n \"isCatalog\": \"true\"\n },\n \"dataAddress\": {\n \"@type\": \"DataAddress\",\n \"type\": \"HttpData\",\n \"baseUrl\": \"{{PROVIDER_QNA_DSP_URL}}/api/dsp\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "Create linked Asset for provider-manufacturing", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"linked-asset-provider-manufacturing\",\n \"@type\": \"CatalogAsset\",\n \"properties\": {\n \"description\": \"This is a linked asset that points to the catalog of the provider's Manufacturing department.\",\n \"isCatalog\": \"true\"\n },\n \"dataAddress\": {\n \"@type\": \"DataAddress\",\n \"type\": \"HttpData\",\n \"baseUrl\": \"{{PROVIDER_MF_DSP_URL}}/api/dsp\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "Create normal asset for CatalogServer", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"normal-asset-1\",\n \"@type\": \"Asset\",\n \"properties\": {\n \"description\": \"This is a conventional asset, not a CatalogAsset.\"\n },\n \"dataAddress\": {\n \"@type\": \"DataAddress\",\n \"type\": \"HttpData\",\n \"baseUrl\": \"https://jsonplaceholder.typicode.com/todos\",\n \"proxyPath\": \"true\",\n \"proxyQueryParams\": \"true\"\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets" - ] - } - }, - "response": [] - }, - { - "name": "Create Membership Policy", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"PolicyDefinition\",\n \"@id\": \"require-membership\",\n \"policy\": {\n \"@type\": \"Set\",\n \"permission\": [\n {\n \"action\": \"use\",\n \"constraint\": {\n \"leftOperand\": \"MembershipCredential\",\n \"operator\": \"eq\",\n \"rightOperand\": \"active\"\n }\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/policydefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "policydefinitions" - ] - } - }, - "response": [] - }, - { - "name": "Create \"require membership\" definition", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@id\": \"membership-required-def\",\n \"@type\": \"ContractDefinition\",\n \"accessPolicyId\": \"require-membership\",\n \"contractPolicyId\": \"require-membership\",\n \"assetsSelector\": {\n \"@type\": \"Criterion\",\n \"operandLeft\": \"https://w3id.org/edc/v0.0.1/ns/id\",\n \"operator\": \"in\",\n \"operandRight\": [\n \"linked-asset-provider-qna\",\n \"linked-asset-provider-manufacturing\",\n \"normal-asset-1\"\n ]\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/contractdefinitions", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "contractdefinitions" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204, 409])", - "})" - ] - } - } - ] - }, - { - "name": "ControlPlane Management", - "item": [ - { - "name": "Get Assets", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": {\n \"@vocab\": \"https://w3id.org/edc/v0.0.1/ns/\"\n },\n \"@type\": \"QuerySpec\"\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/assets/request", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "assets", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Request Catalog", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"CatalogRequest\",\n \"counterPartyAddress\": \"{{CATALOG_SERVER_DSP_URL}}/api/dsp\",\n \"counterPartyId\": \"{{PROVIDER_ID}}\",\n \"protocol\": \"dataspace-protocol-http\",\n \"querySpec\": {\n \"offset\": 0,\n \"limit\": 50\n }\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/catalog/request", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "catalog", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Get Cached Catalogs", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// get the policy id of \"asset-1\" and save it as an environment variable", - "if(pm.response.code < 300 && pm.response.code >= 200){", - " if(pm.response.json().length > 0){", - " const dcat_datasets = pm.response.json()[0][\"dcat:catalog\"][1][\"dcat:dataset\"]", - " const asset_1 = dcat_datasets.find((asset) => asset[\"@id\"] == \"asset-1\")", - " pm.environment.set(\"POLICY_ID_ASSET_1\", asset_1[\"odrl:hasPolicy\"][\"@id\"]);", - " }", - "}", - "", - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "pm.test(\"Policy id for asset 1 is set\", function(){", - " pm.expect(pm.environment.get(\"POLICY_ID_ASSET_1\")).not.to.be.undefined", - "})" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"QuerySpec\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CONSUMER_CATALOG_QUERY_URL}}/api/catalog/v1alpha/catalog/query", - "host": [ - "{{CONSUMER_CATALOG_QUERY_URL}}" - ], - "path": [ - "api", - "catalog", - "v1alpha", - "catalog", - "query" - ] - } - }, - "response": [] - }, - { - "name": "Initiate negotiation", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "if(!pm.environment.has(\"POLICY_ID_ASSET_1\")){", - " throw new Error('Policy-ID of Asset-1 is not yet available, please execute request \"Get Cached Catalog\" first!');", - "}" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "test", - "script": { - "exec": [ - "pm.environment.set(\"CONTRACT_NEGOTIATION_ID\", pm.response.json()[\"@id\"])", - "", - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "pm.test(\"Contract negotiation id is set\", function(){", - " pm.expect(pm.environment.get(\"CONTRACT_NEGOTIATION_ID\")).not.to.be.undefined", - "})" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"ContractRequest\",\n \"counterPartyAddress\": \"{{PROVIDER_DSP_URL}}/api/dsp\",\n \"counterPartyId\": \"{{PROVIDER_ID}}\",\n \"protocol\": \"dataspace-protocol-http\",\n \"policy\": {\n \"@type\": \"Offer\",\n \"@id\": \"{{POLICY_ID_ASSET_1}}\",\n \"assigner\": \"{{PROVIDER_ID}}\",\n \"permission\": [],\n \"prohibition\": [],\n \"obligation\": {\n \"action\": \"use\",\n \"constraint\": {\n \"leftOperand\": \"DataAccess.level\",\n \"operator\": \"eq\",\n \"rightOperand\": \"processing\"\n }\n },\n \"target\": \"asset-1\"\n },\n \"callbackAddresses\": []\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{HOST}}/api/management/v3/contractnegotiations", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "contractnegotiations" - ] - } - }, - "response": [] - }, - { - "name": "Get Contract Negotiations", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// get the contact agreement id and save it as an environment variable", - "if(pm.response.code < 300 && pm.response.code >= 200 && pm.response.json().length > 0){", - " var find_negotiation;", - " if (pm.environment.has(\"CONTRACT_NEGOTIATION_ID\")){", - " find_negotiation = pm.response.json().find((el) => el[\"@id\"] == pm.environment.get(\"CONTRACT_NEGOTIATION_ID\"))", - " }", - "", - " if(find_negotiation){", - " const contractAgreementId = find_negotiation[\"contractAgreementId\"];", - " pm.environment.set(\"CONTRACT_AGREEMENT_ID\", contractAgreementId);", - " }", - "}", - "", - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "pm.test(\"Contract Agreement ID is set\", function(){", - " pm.expect(pm.environment.get(\"CONTRACT_AGREEMENT_ID\")).not.to.be.undefined", - "})", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"QuerySpec\"\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/contractnegotiations/request", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "contractnegotiations", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Initiate Transfer", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "if(!pm.environment.has(\"CONTRACT_AGREEMENT_ID\")){", - " throw new Error('Contract Agreement ID is not yet available, please execute requests \"Initiate Negotiation and Get Contract Negotiation\" first!');", - "}" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"assetId\": \"asset-1\",\n \"counterPartyAddress\": \"{{PROVIDER_DSP_URL}}/api/dsp\",\n \"connectorId\": \"{{PROVIDER_ID}}\",\n \"contractId\": \"{{CONTRACT_AGREEMENT_ID}}\",\n \"dataDestination\": {\n \"type\": \"HttpProxy\"\n },\n \"protocol\": \"dataspace-protocol-http\",\n \"transferType\": \"HttpData-PULL\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{HOST}}/api/management/v3/transferprocesses", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "transferprocesses" - ] - } - }, - "response": [] - }, - { - "name": "Get transfer processes", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"QuerySpec\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{HOST}}/api/management/v3/transferprocesses/request", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "transferprocesses", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Get cached EDRs", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// get the transfer process id of \"asset-1\" and save it as an environment variable if the response body is not empty", - "if(pm.response.code < 300 && pm.response.code >= 200 && pm.response.json().length > 0){", - " const transferProcessId = pm.response.json()[0][\"transferProcessId\"];", - " pm.environment.set(\"TRANSFER_PROCESS_ID\", transferProcessId);", - "}", - "", - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "pm.test(\"Transfer process id is set\", function(){", - " pm.expect(pm.environment.get(\"TRANSFER_PROCESS_ID\")).not.to.be.undefined", - "})", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"@context\": [\n \"https://w3id.org/edc/connector/management/v0.0.1\"\n ],\n \"@type\": \"QuerySpec\"\n}" - }, - "url": { - "raw": "{{HOST}}/api/management/v3/edrs/request", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "edrs", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Get EDR DataAddress for TransferId", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "if(!pm.environment.has(\"TRANSFER_PROCESS_ID\")){", - " throw new Error('Transfer Process ID is not yet available, please execute request \"Get Transfer Processes\" first!');", - "}" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// get the authorization token and save it as an environment variable", - "if(pm.response.code < 300 && pm.response.code >= 200){", - " //using the first authorization token found", - " const authorization = pm.response.json()[\"authorization\"];", - " pm.environment.set(\"AUTHORIZATION\", authorization);", - "}", - "", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{HOST}}/api/management/v3/edrs/{{TRANSFER_PROCESS_ID}}/dataaddress", - "host": [ - "{{HOST}}" - ], - "path": [ - "api", - "management", - "v3", - "edrs", - "{{TRANSFER_PROCESS_ID}}", - "dataaddress" - ] - } - }, - "response": [] - }, - { - "name": "Download Data from Public API", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "if(!pm.environment.has(\"AUTHORIZATION\")){", - " throw new Error(' The authorization token is not yet available, please execute request \"Get EDR DataAddress for TransferId\" first!');", - "}" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "{{AUTHORIZATION}}", - "type": "text" - } - ], - "url": { - "raw": "{{PROVIDER_PUBLIC_API}}/api/public", - "host": [ - "{{PROVIDER_PUBLIC_API}}" - ], - "path": [ - "api", - "public" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "IdentityHub", - "item": [ - { - "name": "Participant Context Mgmt API", - "item": [ - { - "name": "Get Participant By ID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "Get all participants", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants" - ] - } - }, - "response": [] - }, - { - "name": "Create Participant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"roles\":[],\n \"serviceEndpoints\":[],\n \"active\": true,\n \"participantId\": \"{{NEW_PARTICIPANT_ID}}\",\n \"did\": \"{{NEW_PARTICIPANT_ID}}\",\n \"key\":{\n \"keyId\": \"key-1\",\n \"privateKeyAlias\": \"{{NEW_PARTICIPANT_ID}}-alias\",\n \"keyGeneratorParams\":{\n \"algorithm\": \"EdDSA\",\n \"curve\": \"Ed25519\"\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "" - ] - } - }, - "response": [] - }, - { - "name": "Create Participant (existing key)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"roles\":[],\n \"serviceEndpoints\":[],\n \"active\": true,\n \"participantId\": \"{{NEW_PARTICIPANT_ID}}\",\n \"did\": \"{{NEW_PARTICIPANT_ID}}\",\n \"key\":{\n \"keyId\": \"key-1\",\n \"privateKeyAlias\": \"{{NEW_PARTICIPANT_ID}}-alias\",\n \"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5r\\noYnkAXuqCYfNK3ex+hMWFuiXGUxHlzShAehR6wvwzV23bbC0tcFcVgW//A==\\n-----END PUBLIC KEY-----\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "" - ] - } - }, - "response": [] - }, - { - "name": "Update Roles", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "[\n \"role1\", \"role2\", \"admin\"\n]", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/roles", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "roles" - ] - } - }, - "response": [] - }, - { - "name": "Regenerate Token", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/api/admin/v1alpha/attestations?participantId=", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "attestations" - ], - "query": [ - { - "key": "participantId", - "value": "" - } - ] - } - }, - "response": [] - }, - { - "name": "Activate Participant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"roles\":[],\n \"serviceEndpoints\":[],\n \"isActive\": true,\n \"participantId\": \"foobar\",\n \"did\": \"did:web:foobar\",\n \"key\":{\n \"keyId\": \"key1\",\n \"privateKeyAlias\": \"foobar-alias\",\n \"keyGeneratorParams\":{\n \"algorithm\": \"EC\",\n \"curve\": \"secp256r1\"\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/state?isActive=true", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "state" - ], - "query": [ - { - "key": "isActive", - "value": "true" - } - ] - } - }, - "response": [] - }, - { - "name": "Deactivate Participant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"roles\":[],\n \"serviceEndpoints\":[],\n \"isActive\": true,\n \"participantId\": \"foobar\",\n \"did\": \"did:web:foobar\",\n \"key\":{\n \"keyId\": \"key1\",\n \"privateKeyAlias\": \"foobar-alias\",\n \"keyGeneratorParams\":{\n \"algorithm\": \"EC\",\n \"curve\": \"secp256r1\"\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/state?isActive=false", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "state" - ], - "query": [ - { - "key": "isActive", - "value": "false" - } - ] - } - }, - "response": [] - }, - { - "name": "Delete Participant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"roles\":[],\n \"serviceEndpoints\":[],\n \"isActive\": true,\n \"participantId\": \"foobar\",\n \"did\": \"did:web:foobar\",\n \"key\":{\n \"keyId\": \"key1\",\n \"privateKeyAlias\": \"foobar-alias\",\n \"keyGeneratorParams\":{\n \"algorithm\": \"EC\",\n \"curve\": \"secp256r1\"\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "KeyPair Resources Mgmt API", - "item": [ - { - "name": "Get KeyPair for Participant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/BPN0000001/keypairs", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "BPN0000001", - "keypairs" - ] - } - }, - "response": [] - }, - { - "name": "Get all KeyPairs", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/keypairs", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "keypairs" - ] - } - }, - "response": [] - }, - { - "name": "Add KeyPair", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204, 409])", - "})" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"keyId\": \"key6\",\n \"privateKeyAlias\": \"new-foobar-alias5\",\n \"keyGeneratorParams\": {\n \"algorithm\": \"EdDSA\",\n \"curve\": \"Ed25519\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/keypairs", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "keypairs" - ] - } - }, - "response": [] - }, - { - "name": "Rotate key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"keyId\": \"key2\",\n \"privateKeyAlias\": \"new-foobar-alias\",\n \"keyGeneratorParams\": {\n \"algorithm\": \"EC\",\n \"curve\": \"secp256r1\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/keypairs/key1/rotate", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "keypairs", - "key1", - "rotate" - ] - } - }, - "response": [] - }, - { - "name": "Revoke key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/keypairs/key2/revoke", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "keypairs", - "key2", - "revoke" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "DID Document Mgmt API", - "item": [ - { - "name": "Query DIDs", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/BPN0000001/dids/query", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "BPN0000001", - "dids", - "query" - ] - } - }, - "response": [] - }, - { - "name": "Get All DID Documents", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/dids", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "dids" - ] - } - }, - "response": [] - }, - { - "name": "Publish DID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"did:web:BPN0000001\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/dids/publish", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "dids", - "publish" - ] - } - }, - "response": [] - }, - { - "name": "Add endpoint", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"id\": \"some-other-id\",\n \"type\": \"CredentialService\",\n \"serviceEndpoint\": \"https://foobar.myconnector.com\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/dids/{{DID}}/endpoints", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "dids", - "{{DID}}", - "endpoints" - ] - } - }, - "response": [] - }, - { - "name": "Un-Publish DID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"did:web:consumer-identityhub%3A7083:connector1\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/dids/unpublish", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "dids", - "unpublish" - ] - } - }, - "response": [] - }, - { - "name": "Delete DID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"did:web:consumer-identityhub%3A7083:connector1\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/dids", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "dids" - ] - } - }, - "response": [] - }, - { - "name": "Get DID Document state", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"did:web:BPN0000001\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/dids/state", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "dids", - "state" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Status is OK\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204])", - "})" - ] - } - } - ] - }, - { - "name": "VerifiableCredential Mgmt API", - "item": [ - { - "name": "Get Credential By ID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status is OK\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204])", - "})" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/credentials/CREDENTIAL-ID", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID}}", - "credentials", - "CREDENTIAL-ID" - ] - } - }, - "response": [] - }, - { - "name": "Get All Credentials", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status is OK\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204])", - "})" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/credentials", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "Query Credentials by Type", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status is OK\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 204])", - "})" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgz/credentials?type=FoobarCredential", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgz", - "credentials" - ], - "query": [ - { - "key": "type", - "value": "FoobarCredential" - } - ] - } - }, - "response": [] - }, - { - "name": "Make Credential Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if(pm.response.code < 300 && pm.response.code >= 200){", - " if(pm.response.text().length > 0){", - " const requestId = pm.response.text()", - " pm.environment.set(\"REQUEST_ID\", requestId);", - " }", - "}" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"issuerDid\": \"{{ISSUER_DID}}\",\n \"holderPid\": \"credential-request-1\",\n \"credentials\": [{\n \"format\": \"VC1_0_JWT\",\n \"credentialType\": \"FoobarCredential\"\n }]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID_BASE64}}/credentials/request", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID_BASE64}}", - "credentials", - "request" - ] - } - }, - "response": [] - }, - { - "name": "Get Credential Reqeusts", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID_BASE64}}/credentials/request/{{REQUEST_ID}}", - "host": [ - "{{CS_URL}}" - ], - "path": [ - "api", - "identity", - "v1alpha", - "participants", - "{{PARTICIPANT_ID_BASE64}}", - "credentials", - "request", - "{{REQUEST_ID}}" - ] - } - }, - "response": [] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{IH_API_TOKEN}}", - "type": "string" - }, - { - "key": "key", - "value": "X-Api-Key", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204, 409])", - "})" - ] - } - } - ] - }, - { - "name": "IssuerService", - "item": [ - { - "name": "Admin API", - "item": [ - { - "name": "Attestations", - "item": [ - { - "name": "create Attestation", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"database\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "attestations" - ] - }, - "description": "Creates an attestation definition in the runtime." - }, - "response": [ - { - "name": "The attestation definition was added successfully.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations" - ] - } - }, - "status": "Created", - "code": 201, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The participant was not found.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations" - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "Can't add the participant, because a object with the same ID already exists", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"\",\n \"configuration\": {\n \"commodof4\": {},\n \"laborisb27\": {}\n },\n \"id\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations" - ] - } - }, - "status": "Conflict", - "code": 409, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "query Attestations", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations/query", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "attestations", - "query" - ] - }, - "description": "Query attestation definitions" - }, - "response": [ - { - "name": "A list of attestation metadata.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations", - "query" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"attestationType\": \"\",\n \"configuration\": {\n \"aliquip2\": {},\n \"pariatur_b3\": {},\n \"essebab\": {}\n },\n \"id\": \"\"\n },\n {\n \"attestationType\": \"\",\n \"configuration\": {\n \"sint_9f6\": {}\n },\n \"id\": \"\"\n }\n]" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations", - "query" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/attestations/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "attestations", - "query" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - } - ] - }, - { - "name": "CredentialDefinitions", - "item": [ - { - "name": "create Credential Definition", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\",\n \"formats\": [\"VC1_0_JWT\"]\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions" - ] - }, - "description": "Adds a new credential definition." - }, - "response": [ - { - "name": "The credential definition was created successfully.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Created", - "code": 201, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "Can't create the credential definition, because a object with the same ID already exists", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Conflict", - "code": 409, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "query Credential Definitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/query", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions", - "query" - ] - }, - "description": "Gets all credential definitions for a certain query." - }, - "response": [ - { - "name": "A list of credentials definitions.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - "query" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_1_1\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"in_85a\": {},\n \"dolor310\": {},\n \"incididunt_b\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"eud_\": {},\n \"dolor777\": {},\n \"Utb\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n },\n {\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"laborum7\": {},\n \"voluptate726\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"amet_27c\": {},\n \"voluptate8\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n }\n]" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - "query" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - "query" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "get by ID", - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Gets a credential definition by its ID." - }, - "response": [ - { - "name": "The credential definition was found.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential definition was not found.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "update Credential Definition", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions" - ] - }, - "description": "Updates credential definition." - }, - "response": [ - { - "name": "The credential definition was updated successfully.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "Can't update the credential definition because it was not found.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions" - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "delete Credential Definition By Id", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Deletes a credential definition by its ID." - }, - "response": [ - { - "name": "The credential definition was deleted.", - "originalRequest": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"credentialType\": \"\",\n \"dataModel\": \"V_2_0\",\n \"id\": \"\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"\",\n \"mappings\": [\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n },\n {\n \"input\": \"\",\n \"output\": \"\",\n \"required\": \"\"\n }\n ],\n \"rules\": [\n {\n \"configuration\": {\n \"addca\": {}\n },\n \"type\": \"\"\n },\n {\n \"configuration\": {\n \"incididunt_a1f\": {}\n },\n \"type\": \"\"\n }\n ],\n \"validity\": \"\"\n}" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential definition was not found.", - "originalRequest": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentialdefinitions", - ":credentialDefinitionId" - ], - "variable": [ - { - "key": "credentialDefinitionId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - } - ] - }, - { - "name": "Credentials", - "item": [ - { - "name": "query Credentials", - "protocolProfileBehavior": { - "disabledSystemHeaders": {} - }, - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/query", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - "query" - ] - }, - "description": "Query credentials, possibly across multiple participants." - }, - "response": [ - { - "name": "A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - "query" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"credential\": {\n \"credentialSchema\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialStatus\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialSubject\": [\n {\n \"id\": \"\"\n },\n {\n \"id\": \"\"\n }\n ],\n \"dataModelVersion\": \"V_2_0\",\n \"description\": \"\",\n \"expirationDate\": \"\",\n \"id\": \"\",\n \"issuanceDate\": \"\",\n \"issuer\": {\n \"additionalProperties\": {\n \"ex_991\": {}\n },\n \"id\": \"\"\n },\n \"name\": \"\",\n \"type\": [\n \"\",\n \"\"\n ]\n },\n \"format\": \"JSON_LD\"\n },\n {\n \"credential\": {\n \"credentialSchema\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialStatus\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialSubject\": [\n {\n \"id\": \"\"\n },\n {\n \"id\": \"\"\n }\n ],\n \"dataModelVersion\": \"V_1_1\",\n \"description\": \"\",\n \"expirationDate\": \"\",\n \"id\": \"\",\n \"issuanceDate\": \"\",\n \"issuer\": {\n \"additionalProperties\": {\n \"enima7\": {},\n \"ad_a5\": {},\n \"ipsum_65\": {}\n },\n \"id\": \"\"\n },\n \"name\": \"\",\n \"type\": [\n \"\",\n \"\"\n ]\n },\n \"format\": \"JWT\"\n }\n]" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - "query" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - "query" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "resume Credential", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/resume", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - ":credentialId", - "resume" - ], - "variable": [ - { - "key": "credentialId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Resumes a credential with the given ID for the given participant. Resumed credentials will be removed from the Revocation List." - }, - "response": [ - { - "name": "The credential was resumed successfully. Check the Revocation List credential to confirm.", - "originalRequest": { - "method": "POST", - "header": [], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/resume", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "resume" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "No Content", - "code": 204, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/resume", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "resume" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/resume", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "resume" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential or the participant was not found.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/resume", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "resume" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "revoke Credential", - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/revoke", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - ":credentialId", - "revoke" - ], - "variable": [ - { - "key": "credentialId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Revokes a credential with the given ID for the given participant. Revoked credentials will be added to the Revocation List" - }, - "response": [ - { - "name": "The credential was revoked successfully. Check the Revocation List credential to confirm.", - "originalRequest": { - "method": "POST", - "header": [], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/revoke", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "revoke" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "No Content", - "code": 204, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/revoke", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "revoke" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/revoke", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "revoke" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential or the participant was not found.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/revoke", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "revoke" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "check Credential Status", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/status", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - ":credentialId", - "status" - ], - "variable": [ - { - "key": "credentialId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Checks the revocation status of a credential with the given ID for the given participant." - }, - "response": [ - { - "name": "The credential status.", - "originalRequest": { - "method": "GET", - "header": [], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "status" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "status" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "status" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential or the participant was not found.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "status" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "suspend Credential", - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/suspend", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - ":credentialId", - "suspend" - ], - "variable": [ - { - "key": "credentialId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Suspends a credential with the given ID for the given participant. Suspended credentials will be added to the Revocation List. Suspension is reversible." - }, - "response": [ - { - "name": "The credential was suspended successfully. Check the Revocation List credential to confirm.", - "originalRequest": { - "method": "POST", - "header": [], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/suspend", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "suspend" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "No Content", - "code": 204, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/suspend", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "suspend" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/suspend", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "suspend" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The credential or the participant was not found.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:credentialId/suspend", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":credentialId", - "suspend" - ], - "variable": [ - { - "key": "credentialId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "get Credentials", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/{{PARTICIPANT_ID}}", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentials", - "{{PARTICIPANT_ID}}" - ] - }, - "description": "Gets all credentials for a certain participant." - }, - "response": [ - { - "name": "A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"credential\": {\n \"credentialSchema\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialStatus\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialSubject\": [\n {\n \"id\": \"\"\n },\n {\n \"id\": \"\"\n }\n ],\n \"dataModelVersion\": \"V_2_0\",\n \"description\": \"\",\n \"expirationDate\": \"\",\n \"id\": \"\",\n \"issuanceDate\": \"\",\n \"issuer\": {\n \"additionalProperties\": {\n \"ex_991\": {}\n },\n \"id\": \"\"\n },\n \"name\": \"\",\n \"type\": [\n \"\",\n \"\"\n ]\n },\n \"format\": \"JSON_LD\"\n },\n {\n \"credential\": {\n \"credentialSchema\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialStatus\": [\n {\n \"id\": \"\",\n \"type\": \"\"\n },\n {\n \"id\": \"\",\n \"type\": \"\"\n }\n ],\n \"credentialSubject\": [\n {\n \"id\": \"\"\n },\n {\n \"id\": \"\"\n }\n ],\n \"dataModelVersion\": \"V_1_1\",\n \"description\": \"\",\n \"expirationDate\": \"\",\n \"id\": \"\",\n \"issuanceDate\": \"\",\n \"issuer\": {\n \"additionalProperties\": {\n \"enima7\": {},\n \"ad_a5\": {},\n \"ipsum_65\": {}\n },\n \"id\": \"\"\n },\n \"name\": \"\",\n \"type\": [\n \"\",\n \"\"\n ]\n },\n \"format\": \"JWT\"\n }\n]" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The participant was not found.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/credentials/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "credentials", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - } - ] - }, - { - "name": "Participants", - "item": [ - { - "name": "update Participant", - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Updates participant data." - }, - "response": [ - { - "name": "The participant was updated successfully.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "Can't update the participant because it was not found.", - "originalRequest": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "Create Participant", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"did:web:test\",\n \"participantId\": \"did:web:test\",\n \"name\": \"Test Participant\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Adds a new participant." - }, - "response": [ - { - "name": "The participant was added successfully.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Created", - "code": 201, - "_postman_previewlanguage": "text", - "header": [], - "cookie": [], - "body": "" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "Can't add the participant, because a object with the same ID already exists", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants" - ] - } - }, - "status": "Conflict", - "code": 409, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "query Participants", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders/query", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders", - "query" - ] - }, - "description": "Gets all participants for a certain query." - }, - "response": [ - { - "name": "A list of participant metadata.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - "query" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n },\n {\n \"did\": \"\",\n \"participantId\": \"\",\n \"name\": \"\"\n }\n]" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - "query" - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - "query" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The participant was not found.", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"filterExpression\": [\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n },\n {\n \"operandLeft\": {},\n \"operandRight\": {},\n \"operator\": \"\"\n }\n ],\n \"limit\": \"\",\n \"offset\": \"\",\n \"sortField\": \"\",\n \"sortOrder\": \"DESC\"\n}", - "options": { - "raw": { - "headerFamily": "json", - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/query", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - "query" - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - }, - { - "name": "get Participant By Id", - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders/:participantId", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders", - ":participantId" - ], - "variable": [ - { - "key": "participantId", - "value": "", - "description": "(Required) " - } - ] - }, - "description": "Gets metadata for a certain participant." - }, - "response": [ - { - "name": "A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "{\n \"attestations\": [\n \"\",\n \"\"\n ],\n \"did\": \"\",\n \"participantId\": \"\",\n \"participantName\": \"\"\n}" - }, - { - "name": "Request body was malformed, or the request could not be processed", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The request could not be completed, because either the authentication was missing or was not valid.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - }, - { - "name": "The participant was not found.", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/v1alpha/participants/:participantId", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "v1alpha", - "participants", - ":participantId" - ], - "variable": [ - { - "key": "participantId" - } - ] - } - }, - "status": "Not Found", - "code": 404, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "cookie": [], - "body": "[\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n },\n {\n \"invalidValue\": {},\n \"message\": \"\",\n \"path\": \"\",\n \"type\": \"\"\n }\n]" - } - ] - } - ] - }, - { - "name": "IssuanceProcesses", - "item": [ - { - "name": "Query issuance processes", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/issuanceprocesses/query", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "issuanceprocesses", - "query" - ] - } - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Seed Issuer SQL", - "item": [ - { - "name": "Create consumer participant", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"{{CONSUMER_ID}}\",\n \"holderId\": \"{{CONSUMER_ID}}\",\n \"name\": \"{{CONSUMER_NAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/holders' \\\n-H 'Content-Type: application/json' \\\n-d '{ \"did\": \"did:web:localhost%3A7083\", \"participantId\": \"did:web:localhost%3A7083\", \"name\": \"Consumer Participant\"}'" - }, - "response": [] - }, - { - "name": "Create provider participant", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"{{PROVIDER_ID}}\",\n \"holderId\": \"{{PROVIDER_ID}}\",\n \"name\": \"{{PROVIDER_NAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/holders' \\\n-H 'Content-Type: application/json' \\\n-d '{ \"did\": \"did:web:localhost%3A7093\", \"participantId\": \"did:web:localhost%3A7093\", \"name\": \"Provider Participant\"}'" - }, - "response": [] - }, - { - "name": "Create attestation definitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"database\",\n \"configuration\": {\n \"tableName\": \"membership_attestations\",\n \"dataSourceName\": \"membership\",\n \"idColumn\": \"holder_id\"\n },\n \"id\": \"db-attestation-def-1\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "attestations" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/attestations' \\\n-H 'Content-Type: application/json' \\\n-d '{\n \"attestationType\": \"demo\",\n \"configuration\": {\n },\n \"id\": \"demo-attestation-def-1\"\n}'" - }, - "response": [] - }, - { - "name": "Create credential definitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"db-attestation-def-1\"\n ],\n \"credentialType\": \"FoobarCredential\",\n \"id\": \"demo-credential-def-2\",\n \"jsonSchema\": \"{}\",\n \"jsonSchemaUrl\": \"https://example.com/schema/demo-credential.json\",\n \"mappings\": [\n {\n \"input\": \"membership_type\",\n \"output\": \"credentialSubject.membershipType\",\n \"required\": \"true\"\n },\n {\n \"input\": \"membership_start_date\",\n \"output\": \"credentialSubject.membershipStartDate\",\n \"required\": true\n }\n ],\n \"rules\": [],\n \"format\": \"VC1_0_JWT\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/credentialdefinitions' \\\n-H 'Content-Type: application/json' \\\n-d '{\n \"attestations\": [\n \"demo-attestation-def-1\"\n ],\n \"credentialType\": \"DemoCredential\",\n \"dataModel\": \"V_1_1\",\n \"id\": \"demo-credential-def-1\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"https://example.com/schema/demo-credential.json\",\n \"mappings\": [\n {\n \"input\": \"name\",\n \"output\": \"credentialSubject.name\",\n \"required\": \"true\"\n }\n ],\n \"rules\": []\n }'" - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204, 409])", - "})" - ] - } - } - ] - }, - { - "name": "Seed Issuer", - "item": [ - { - "name": "Create consumer participant", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"{{CONSUMER_ID}}\",\n \"holderId\": \"{{CONSUMER_ID}}\",\n \"name\": \"{{CONSUMER_NAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/holders' \\\n-H 'Content-Type: application/json' \\\n-d '{ \"did\": \"did:web:localhost%3A7083\", \"participantId\": \"did:web:localhost%3A7083\", \"name\": \"Consumer Participant\"}'" - }, - "response": [] - }, - { - "name": "Create provider participant", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"did\": \"{{PROVIDER_ID}}\",\n \"holderId\": \"{{PROVIDER_ID}}\",\n \"name\": \"{{PROVIDER_NAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "holders" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/holders' \\\n-H 'Content-Type: application/json' \\\n-d '{ \"did\": \"did:web:localhost%3A7093\", \"participantId\": \"did:web:localhost%3A7093\", \"name\": \"Provider Participant\"}'" - }, - "response": [] - }, - { - "name": "Create attestation definitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestationType\": \"demo\",\n \"configuration\": {\n },\n \"id\": \"demo-attestation-def-1\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "attestations" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/attestations' \\\n-H 'Content-Type: application/json' \\\n-d '{\n \"attestationType\": \"demo\",\n \"configuration\": {\n },\n \"id\": \"demo-attestation-def-1\"\n}'" - }, - "response": [] - }, - { - "name": "Create credential definitions", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"attestations\": [\n \"demo-attestation-def-1\"\n ],\n \"credentialType\": \"DemoCredential\",\n \"id\": \"demo-credential-def-1\",\n \"jsonSchema\": \"{}\",\n \"jsonSchemaUrl\": \"https://example.com/schema/demo-credential.json\",\n \"mappings\": [\n {\n \"input\": \"participant.name\",\n \"output\": \"credentialSubject.participant_name\",\n \"required\": \"true\"\n }\n ],\n \"rules\": [],\n \"format\": \"VC1_0_JWT\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions", - "host": [ - "{{ISSUER_ADMIN_URL}}" - ], - "path": [ - "api", - "admin", - "v1alpha", - "participants", - "{{ISSUER_CONTEXT_ID}}", - "credentialdefinitions" - ] - }, - "description": "Generated from cURL: curl -sL -X POST 'http://localhost:10013/api/admin/v1alpha/credentialdefinitions' \\\n-H 'Content-Type: application/json' \\\n-d '{\n \"attestations\": [\n \"demo-attestation-def-1\"\n ],\n \"credentialType\": \"DemoCredential\",\n \"dataModel\": \"V_1_1\",\n \"id\": \"demo-credential-def-1\",\n \"jsonSchema\": \"\",\n \"jsonSchemaUrl\": \"https://example.com/schema/demo-credential.json\",\n \"mappings\": [\n {\n \"input\": \"name\",\n \"output\": \"credentialSubject.name\",\n \"required\": \"true\"\n }\n ],\n \"rules\": []\n }'" - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "pm.test(\"Status is OK or conflict\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204, 409])", - "})" - ] - } - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{ISSUER_ADMIN_API_TOKEN}}", - "type": "string" - }, - { - "key": "key", - "value": "x-api-key", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "password", - "type": "string" - }, - { - "key": "key", - "value": "X-Api-Key", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if(pm.request.method == \"POST\" || pm.request.method == \"PUT\"){", - " pm.request.headers.add(\"Content-Type: application/json\");", - "}" - ] - } - }, - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "key": "HOST", - "value": "http://localhost:8081", - "type": "string" - }, - { - "key": "PROVIDER_DSP", - "value": "http://localhost:8092", - "type": "string" - }, - { - "key": "PROVIDER_ID", - "value": "did:web:localhost%3A7093", - "type": "string" - }, - { - "key": "CS_URL", - "value": "http://localhost:8181", - "type": "string" - }, - { - "key": "ISSUER_ADMIN_URL", - "value": "http://localhost:10013", - "type": "string" - }, - { - "key": "IH_API_TOKEN", - "value": "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=", - "type": "string" - }, - { - "key": "ISSUER_ADMIN_API_TOKEN", - "value": "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=", - "type": "string" - }, - { - "key": "PARTICIPANT_ID", - "value": "super-user" - }, - { - "key": "DID", - "value": "did:web:super-user" - }, - { - "key": "NEW_PARTICIPANT_ID", - "value": "did:web:localhost%3A7083", - "type": "string" - }, - { - "key": "CATALOG_URL", - "value": "http://localhost:8084", - "type": "string" - }, - { - "key": "TED_DSP_URL", - "value": "", - "type": "string" - }, - { - "key": "ISSUER_CONTEXT_ID", - "value": "ZGlkOndlYjpsb2NhbGhvc3QlM0ExMDEwMA==", - "type": "string" - }, - { - "key": "ISSUER_DID", - "value": "did:web:localhost%3A10100", - "type": "string" - }, - { - "key": "PARTICIPANT_ID_BASE64", - "value": "ZGlkOndlYjpsb2NhbGhvc3QlM0E3MDgz", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/deployment/postman/http-client.env.json b/deployment/postman/http-client.env.json deleted file mode 100644 index 0550d19b4..000000000 --- a/deployment/postman/http-client.env.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Local": { - "CS_URL": "http://localhost:7082", - "HOST": "http://localhost:8081", - "PARTICIPANT_ID": "super-user", - "CATALOG_URL": "http://localhost:8084", - "DID": "did:web:super-user" - } -} diff --git a/deployment/provider.tf b/deployment/provider.tf deleted file mode 100644 index 454fa76ff..000000000 --- a/deployment/provider.tf +++ /dev/null @@ -1,163 +0,0 @@ -# -# Copyright (c) 2024 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 -# - - -# This file deploys all the components needed for the provider side of the scenario, -# i.e. a catalog server ("bob"), two connectors ("ted" and "carol") as well as one identityhub and one vault - -# first provider connector "provider-qna" -module "provider-qna-connector" { - source = "./modules/connector" - humanReadableName = "provider-qna" - participantId = var.provider-did - database = { - user = "qna" - password = "provider-qna" - url = "jdbc:postgresql://${module.provider-postgres.database-url}/provider_qna" - } - namespace = kubernetes_namespace.ns.metadata.0.name - vault-url = "http://provider-vault:8200" - sts-token-url = "${module.provider-identityhub.sts-token-url}/token" - useSVE = var.useSVE -} - -# Second provider connector "provider-manufacturing" -module "provider-manufacturing-connector" { - source = "./modules/connector" - humanReadableName = "provider-manufacturing" - participantId = var.provider-did - database = { - user = "manufacturing" - password = "provider-manufacturing" - url = "jdbc:postgresql://${module.provider-postgres.database-url}/provider_manufacturing" - } - namespace = kubernetes_namespace.ns.metadata.0.name - vault-url = "http://provider-vault:8200" - sts-token-url = "${module.provider-identityhub.sts-token-url}/token" - useSVE = var.useSVE -} - -module "provider-identityhub" { - depends_on = [module.provider-vault] - source = "./modules/identity-hub" - credentials-dir = dirname("./assets/credentials/k8s/provider/") - humanReadableName = "provider-identityhub" - # must be named "provider-identityhub" until we regenerate DIDs and credentials - participantId = var.provider-did - vault-url = "http://provider-vault:8200" - service-name = "provider" - namespace = kubernetes_namespace.ns.metadata.0.name - - database = { - user = "identity" - password = "identity" - url = "jdbc:postgresql://${module.provider-postgres.database-url}/identity" - } - useSVE = var.useSVE -} - -# Catalog server runtime -module "provider-catalog-server" { - source = "./modules/catalog-server" - humanReadableName = "provider-catalog-server" - participantId = var.provider-did - namespace = kubernetes_namespace.ns.metadata.0.name - vault-url = "http://provider-vault:8200" - sts-token-url = "${module.provider-identityhub.sts-token-url}/token" - - database = { - user = "catalog_server" - password = "catalog_server" - url = "jdbc:postgresql://${module.provider-postgres.database-url}/catalog_server" - } - useSVE = var.useSVE -} - -module "provider-vault" { - source = "./modules/vault" - humanReadableName = "provider-vault" - namespace = kubernetes_namespace.ns.metadata.0.name -} - -# Postgres database for the consumer -module "provider-postgres" { - depends_on = [kubernetes_config_map.postgres-initdb-config-cs] - source = "./modules/postgres" - instance-name = "provider" - init-sql-configs = [ - kubernetes_config_map.postgres-initdb-config-cs.metadata[0].name, - kubernetes_config_map.postgres-initdb-config-pqna.metadata[0].name, - kubernetes_config_map.postgres-initdb-config-pm.metadata[0].name, - kubernetes_config_map.postgres-initdb-config-ih.metadata[0].name, - ] - namespace = kubernetes_namespace.ns.metadata.0.name -} - -resource "kubernetes_config_map" "postgres-initdb-config-cs" { - metadata { - name = "cs-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "cs-initdb-config.sql" = <<-EOT - CREATE USER catalog_server WITH ENCRYPTED PASSWORD 'catalog_server' SUPERUSER; - CREATE DATABASE catalog_server; - \c catalog_server - - EOT - } -} - -resource "kubernetes_config_map" "postgres-initdb-config-pqna" { - metadata { - name = "provider-qna-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "provider-qna-initdb-config.sql" = <<-EOT - CREATE USER qna WITH ENCRYPTED PASSWORD 'provider-qna' SUPERUSER; - CREATE DATABASE provider_qna; - \c provider_qna - - EOT - } -} - -resource "kubernetes_config_map" "postgres-initdb-config-pm" { - metadata { - name = "provider-manufacturing-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "provider-manufacturing-initdb-config.sql" = <<-EOT - CREATE USER manufacturing WITH ENCRYPTED PASSWORD 'provider-manufacturing' SUPERUSER; - CREATE DATABASE provider_manufacturing; - \c provider_manufacturing - - EOT - } -} - -resource "kubernetes_config_map" "postgres-initdb-config-ih" { - metadata { - name = "ih-initdb-config" - namespace = kubernetes_namespace.ns.metadata.0.name - } - data = { - "ih-initdb-config.sql" = <<-EOT - CREATE USER identity WITH ENCRYPTED PASSWORD 'identity' SUPERUSER; - CREATE DATABASE identity; - \c identity - EOT - } -} \ No newline at end of file diff --git a/deployment/variables.tf b/deployment/variables.tf deleted file mode 100644 index 8b636a5ed..000000000 --- a/deployment/variables.tf +++ /dev/null @@ -1,32 +0,0 @@ -# -# Copyright (c) 2023 Contributors to the Eclipse Foundation -# -# 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 -# - -variable "consumer-did" { - default = "did:web:consumer-identityhub%3A7083:consumer" -} - -variable "provider-did" { - default = "did:web:provider-identityhub%3A7083:provider" -} - -variable "useSVE" { - type = bool - description = "If true, the -XX:UseSVE=0 switch (Scalable Vector Extensions) will be added to the JAVA_TOOL_OPTIONS. Can help on macOs on Apple Silicon processors" - default = false -} \ No newline at end of file diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java index 447f1b9f3..3e724e321 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java @@ -20,7 +20,6 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import java.util.Base64; import java.util.List; import java.util.UUID; @@ -38,19 +37,34 @@ @EndToEndTest public class CredentialIssuanceEndToEndTest { - private static final String CONSUMER_IDENTITYHUB_IDENTITY_URL = "http://127.0.0.1/consumer/cs/"; - private static final String PARTICIPANT_CONTEXT_ID = "did:web:consumer-identityhub%3A7083:consumer"; - private static final String ISSUER_DID = "did:web:dataspace-issuer-service%3A10016:issuer"; + private static final String CONSUMER_IDENTITYHUB_IDENTITY_URL = "http://ih.consumer.localhost:8080/cs/"; + private static final String PARTICIPANT_CONTEXT_ID = "consumer-participant"; +// private static final String PARTICIPANT_CONTEXT_ID = "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer"; + private static final String ISSUER_DID = "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer"; private static final String HOLDER_PID = UUID.randomUUID().toString(); + private static final String KEYCLOAK_URL = "http://keycloak.localhost:8080"; private static RequestSpecification baseRequest() { return given() - .header("X-Api-Key", "c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=") + .header("Authorization", "Bearer " + adminToken()) .contentType(JSON) .baseUri(CONSUMER_IDENTITYHUB_IDENTITY_URL) .when(); } + private static String adminToken() { + return given() + .contentType("application/x-www-form-urlencoded") + .formParam("client_id", "admin") + .formParam("client_secret", "edc-v-admin-secret") + .formParam("grant_type", "client_credentials") + .post(KEYCLOAK_URL + "/realms/mvd/protocol/openid-connect/token") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().jsonPath().getString("access_token"); + } + @Test void makeCredentialRequest_expectCredential() { var location = baseRequest() @@ -59,11 +73,11 @@ void makeCredentialRequest_expectCredential() { "issuerDid": "%s", "holderPid": "%s", "credentials": [ - {"format": "VC1_0_JWT", "type": "FoobarCredential", "id": "demo-credential-def-2"} + {"format": "VC1_0_JWT", "type": "MembershipCredential", "id": "membership-credential-def"} ] } """.formatted(ISSUER_DID, HOLDER_PID)) - .post("/api/identity/v1alpha/participants/%s/credentials/request".formatted(base64(PARTICIPANT_CONTEXT_ID))) + .post("/api/identity/v1alpha/participants/%s/credentials/request".formatted(PARTICIPANT_CONTEXT_ID)) .then() .log().ifValidationFails() .statusCode(201) @@ -78,7 +92,7 @@ void makeCredentialRequest_expectCredential() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { baseRequest() - .get("/api/identity/v1alpha/participants/%s/credentials/request/%s".formatted(base64(PARTICIPANT_CONTEXT_ID), requestId)) + .get("/api/identity/v1alpha/participants/%s/credentials/request/%s".formatted(PARTICIPANT_CONTEXT_ID, requestId)) .then() .log().ifValidationFails() .statusCode(200) @@ -91,12 +105,7 @@ void makeCredentialRequest_expectCredential() { .jsonPath() .getList("verifiableCredential.credential.type"); - assertThat(list).anySatisfy(typesList -> assertThat(typesList).containsExactlyInAnyOrder("FoobarCredential", "VerifiableCredential")); - - - } - - private String base64(String input) { - return Base64.getUrlEncoder().encodeToString(input.getBytes()); + assertThat(list).anySatisfy(typesList -> + assertThat(typesList).containsExactlyInAnyOrder("MembershipCredential", "VerifiableCredential")); } } From 23df0a130d6778c941ba3788343d90eead2a9f99 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 10:24:17 +0100 Subject: [PATCH 16/22] update GH workflow --- .github/workflows/run-e2e-tests.yml | 65 +++++++++++------------------ 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index bf3043719..6c94f3dc3 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -49,74 +49,59 @@ jobs: - name: "Setup Kubectl" uses: azure/setup-kubectl@v4 - - - name: "Set up OpenTofu" - uses: opentofu/setup-opentofu@v2 - - uses: actions/checkout@v6 - - uses: eclipse-edc/.github/.github/actions/setup-build@main - name: "Build runtime images" working-directory: ./ run: | - ./gradlew -Ppersistence=true dockerize + ./gradlew dockerize - name: "Create k8s Kind Cluster" uses: helm/kind-action@v1.14.0 with: - config: deployment/kind.config.yaml cluster_name: dcp-demo - name: "Load runtime images into KinD" - run: kind load docker-image controlplane:latest dataplane:latest identity-hub:latest catalog-server:latest issuerservice:latest -n dcp-demo + run: kind load docker-image -n dcp-demo \ + ghcr.io/eclipse-edc/mvd/controlplane:latest \ + ghcr.io/eclipse-edc/mvd/dataplane:latest \ + ghcr.io/eclipse-edc/mvd/identity-hub:latest \ + ghcr.io/eclipse-edc/mvd/issuerservice:latest - - name: "Install nginx ingress controller" + - name: "Install Traefik Gateway controller" run: |- - echo "::notice title=nginx ingress on KinD::For details how to run nginx ingress on KinD check https://kind.sigs.k8s.io/docs/user/ingress/#ingress-nginx" - 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 + helm repo add traefik https://traefik.github.io/charts + helm repo update + helm upgrade --install --namespace traefik traefik traefik/traefik --create-namespace -f values.yaml + # Wait for traefik to be ready + kubectl rollout status deployment/traefik -n traefik --timeout=120s + # forward port 80 -> 8080 + kubectl -n traefik port-forward svc/traefik 8080:80 & - - name: "Tofu init" - working-directory: ./deployment - run: |- - tofu init -reconfigure - - name: "Tofu plan" - working-directory: ./deployment - run: |- - tofu plan -out=$GITHUB_SHA.out + # install Gateway API CRDs + kubectl apply --server-side --force-conflicts -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/experimental-install.yaml - - name: "Tofu apply" - working-directory: ./deployment + sleep 5 # to be safe` + + - name: "Deploy MVD" run: |- - tofu apply "$GITHUB_SHA.out" + kubectl apply -k k8s/ - - name: "Seed dataspace" + - name: "Wait for MVD to be ready" run: |- - chmod +x seed-k8s.sh - ./seed-k8s.sh + kubectl wait -A \ + --selector=type=edc-job \ + --for=condition=complete job --all \ + --timeout=90s - name: "Run E2E Test" run: |- ./gradlew -DincludeTags="EndToEndTest" test -DverboseTest=true - - name: "Run Newman" - continue-on-error: true - working-directory: ./deployment/postman - run: |- - newman run "MVD.postman_collection.json" -e "MVD K8S.postman_environment.json" --folder "ControlPlane Management" --delay-request 5000 --verbose - - - name: "Print log if test failed" - if: failure() - run: |- - kubectl logs deployment/provider-qna-controlplane -n mvd - - name: "Destroy the KinD cluster" run: >- kind delete cluster -n dcp-demo From 53d5e021c950f65dc5220b7cc1d19e1473dffae0 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 10:31:37 +0100 Subject: [PATCH 17/22] license headers --- .github/workflows/run-e2e-tests.yml | 4 ++-- .github/workflows/verify.yaml | 10 ---------- k8s/common/gateway-class.yaml | 2 +- k8s/common/gateway.yaml | 2 +- k8s/common/keycloak.yaml | 2 +- k8s/common/kustomization.yaml | 13 +++++++++++++ k8s/common/postgres.yaml | 2 +- k8s/consumer/application/controlplane-config.yaml | 2 +- k8s/consumer/application/controlplane-seed.yaml | 12 ++++++++++++ k8s/consumer/application/identityhub-config.yaml | 2 +- k8s/consumer/base/postgres.yaml | 2 +- k8s/issuer/application/issuerservice-config.yaml | 2 +- k8s/issuer/application/issuerservice.yaml | 2 +- k8s/issuer/base/gateway.yaml | 2 +- k8s/issuer/base/postgres.yaml | 2 +- k8s/issuer/kustomization.yml | 13 +++++++++++++ k8s/kustomization.yml | 13 +++++++++++++ k8s/provider/application/controlplane-config.yaml | 2 +- k8s/provider/application/dataplane-config.yaml | 2 +- k8s/provider/application/dataplane.yaml | 2 +- k8s/provider/application/identityhub-config.yaml | 2 +- k8s/provider/base/gateway.yaml | 2 +- k8s/provider/base/postgres.yaml | 2 +- 23 files changed, 70 insertions(+), 29 deletions(-) diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 6c94f3dc3..5b8eeee89 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -57,14 +57,14 @@ jobs: run: | ./gradlew dockerize - - name: "Create k8s Kind Cluster" uses: helm/kind-action@v1.14.0 with: cluster_name: dcp-demo - name: "Load runtime images into KinD" - run: kind load docker-image -n dcp-demo \ + run: | + kind load docker-image -n dcp-demo \ ghcr.io/eclipse-edc/mvd/controlplane:latest \ ghcr.io/eclipse-edc/mvd/dataplane:latest \ ghcr.io/eclipse-edc/mvd/identity-hub:latest \ diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 61707ba9d..401863222 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -62,13 +62,3 @@ jobs: - uses: eclipse-edc/.github/.github/actions/setup-build@main - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest - - validate-terraform: - runs-on: ubuntu-latest - steps: - - uses: hashicorp/setup-terraform@v4 - - uses: actions/checkout@v6 - - name: Check Terraform files are properly formatted (run "terraform fmt -recursive" to fix) - run: | - terraform fmt -recursive - git diff --exit-code \ No newline at end of file diff --git a/k8s/common/gateway-class.yaml b/k8s/common/gateway-class.yaml index 2a209380e..c6d0744a5 100644 --- a/k8s/common/gateway-class.yaml +++ b/k8s/common/gateway-class.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/common/gateway.yaml b/k8s/common/gateway.yaml index 32158716d..9f70e6beb 100644 --- a/k8s/common/gateway.yaml +++ b/k8s/common/gateway.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/common/keycloak.yaml b/k8s/common/keycloak.yaml index 5cf2d1c8b..443c2b3df 100644 --- a/k8s/common/keycloak.yaml +++ b/k8s/common/keycloak.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/common/kustomization.yaml b/k8s/common/kustomization.yaml index e7f8ace2b..af8786dae 100644 --- a/k8s/common/kustomization.yaml +++ b/k8s/common/kustomization.yaml @@ -1,3 +1,16 @@ +# +# Copyright (c) 2026 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 +# + resources: - namespace.yaml - gateway-class.yaml diff --git a/k8s/common/postgres.yaml b/k8s/common/postgres.yaml index 90a89b035..1229873cb 100644 --- a/k8s/common/postgres.yaml +++ b/k8s/common/postgres.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/consumer/application/controlplane-config.yaml b/k8s/consumer/application/controlplane-config.yaml index 28b50fd90..c69107b2a 100644 --- a/k8s/consumer/application/controlplane-config.yaml +++ b/k8s/consumer/application/controlplane-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml index 39986011e..335096559 100644 --- a/k8s/consumer/application/controlplane-seed.yaml +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -1,3 +1,15 @@ +# +# Copyright (c) 2026 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 +# --- apiVersion: batch/v1 diff --git a/k8s/consumer/application/identityhub-config.yaml b/k8s/consumer/application/identityhub-config.yaml index 072366cec..c117059b1 100644 --- a/k8s/consumer/application/identityhub-config.yaml +++ b/k8s/consumer/application/identityhub-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/consumer/base/postgres.yaml b/k8s/consumer/base/postgres.yaml index 5f4c3f659..d061205de 100644 --- a/k8s/consumer/base/postgres.yaml +++ b/k8s/consumer/base/postgres.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/issuer/application/issuerservice-config.yaml b/k8s/issuer/application/issuerservice-config.yaml index d25b46c6c..96b286be2 100644 --- a/k8s/issuer/application/issuerservice-config.yaml +++ b/k8s/issuer/application/issuerservice-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/issuer/application/issuerservice.yaml b/k8s/issuer/application/issuerservice.yaml index 0b0253afc..6a0a57905 100644 --- a/k8s/issuer/application/issuerservice.yaml +++ b/k8s/issuer/application/issuerservice.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/issuer/base/gateway.yaml b/k8s/issuer/base/gateway.yaml index 87bd76e40..1d17b316a 100644 --- a/k8s/issuer/base/gateway.yaml +++ b/k8s/issuer/base/gateway.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/issuer/base/postgres.yaml b/k8s/issuer/base/postgres.yaml index 626f39136..0da789428 100644 --- a/k8s/issuer/base/postgres.yaml +++ b/k8s/issuer/base/postgres.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/issuer/kustomization.yml b/k8s/issuer/kustomization.yml index 0a8321a83..fb442b56d 100644 --- a/k8s/issuer/kustomization.yml +++ b/k8s/issuer/kustomization.yml @@ -1,3 +1,16 @@ +# +# Copyright (c) 2026 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 +# + resources: - base/namespace.yaml - base/vault.yaml diff --git a/k8s/kustomization.yml b/k8s/kustomization.yml index fb0255234..8f371cc0a 100644 --- a/k8s/kustomization.yml +++ b/k8s/kustomization.yml @@ -1,3 +1,16 @@ +# +# Copyright (c) 2026 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 +# + resources: - common - ./consumer diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml index 11d6f8be6..29d43a6fc 100644 --- a/k8s/provider/application/controlplane-config.yaml +++ b/k8s/provider/application/controlplane-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/provider/application/dataplane-config.yaml b/k8s/provider/application/dataplane-config.yaml index 5d44fb98d..42ddce066 100644 --- a/k8s/provider/application/dataplane-config.yaml +++ b/k8s/provider/application/dataplane-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/provider/application/dataplane.yaml b/k8s/provider/application/dataplane.yaml index cc0a0b96a..c5f1a7bfa 100644 --- a/k8s/provider/application/dataplane.yaml +++ b/k8s/provider/application/dataplane.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/provider/application/identityhub-config.yaml b/k8s/provider/application/identityhub-config.yaml index 16fde7310..c3d8d423a 100644 --- a/k8s/provider/application/identityhub-config.yaml +++ b/k8s/provider/application/identityhub-config.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/provider/base/gateway.yaml b/k8s/provider/base/gateway.yaml index 154e155e9..061855133 100644 --- a/k8s/provider/base/gateway.yaml +++ b/k8s/provider/base/gateway.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 diff --git a/k8s/provider/base/postgres.yaml b/k8s/provider/base/postgres.yaml index 669280082..fa468906a 100644 --- a/k8s/provider/base/postgres.yaml +++ b/k8s/provider/base/postgres.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Metaform Systems, Inc. +# Copyright (c) 2026 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 From 138160123a2d3b5ccf4eb2f1d545a6dc8e94bf65 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 15:06:19 +0100 Subject: [PATCH 18/22] update helm version --- .github/workflows/run-e2e-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 5b8eeee89..fbb60f5f7 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -44,8 +44,6 @@ jobs: - name: "Setup Helm" uses: azure/setup-helm@v4 - with: - version: v3.8.1 - name: "Setup Kubectl" uses: azure/setup-kubectl@v4 From 2a1831253f678f65da7a7d80cb7f4138756daeb2 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 15:08:45 +0100 Subject: [PATCH 19/22] checkstyle --- .../DataplaneRegistrationApiController.java | 1 - .../CredentialIssuanceEndToEndTest.java | 1 - .../demo/tests/transfer/CatalogResponse.java | 72 ++++++++++++++----- .../tests/transfer/TransferEndToEndTest.java | 28 -------- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java index cd2274da7..641f73271 100644 --- a/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java +++ b/extensions/data-plane-registration/src/main/java/org/eclipse/edc/connector/dataplane/api/controller/DataplaneRegistrationApiController.java @@ -17,7 +17,6 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import org.eclipse.edc.connector.dataplane.selector.spi.DataPlaneSelectorService; import org.eclipse.edc.connector.dataplane.selector.spi.instance.DataPlaneInstance; diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java index 3e724e321..85052d491 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/issuance/CredentialIssuanceEndToEndTest.java @@ -39,7 +39,6 @@ public class CredentialIssuanceEndToEndTest { private static final String CONSUMER_IDENTITYHUB_IDENTITY_URL = "http://ih.consumer.localhost:8080/cs/"; private static final String PARTICIPANT_CONTEXT_ID = "consumer-participant"; -// private static final String PARTICIPANT_CONTEXT_ID = "did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer"; private static final String ISSUER_DID = "did:web:issuerservice.issuer.svc.cluster.local%3A10016:issuer"; private static final String HOLDER_PID = UUID.randomUUID().toString(); private static final String KEYCLOAK_URL = "http://keycloak.localhost:8080"; diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java index 2750108f8..0c796450b 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/CatalogResponse.java @@ -34,13 +34,21 @@ public class CatalogResponse { @JsonProperty("service") private List services; - public String getId() { return id; } + public String getId() { + return id; + } - public String getParticipantId() { return participantId; } + public String getParticipantId() { + return participantId; + } - public List getDatasets() { return datasets; } + public List getDatasets() { + return datasets; + } - public List getServices() { return services; } + public List getServices() { + return services; + } @JsonIgnoreProperties(ignoreUnknown = true) public static class Dataset { @@ -57,13 +65,21 @@ public static class Dataset { @JsonProperty("distribution") private List distributions; - public String getId() { return id; } + public String getId() { + return id; + } - public String getDescription() { return description; } + public String getDescription() { + return description; + } - public List getPolicies() { return policies; } + public List getPolicies() { + return policies; + } - public List getDistributions() { return distributions; } + public List getDistributions() { + return distributions; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -75,9 +91,13 @@ public static class Offer { @JsonProperty("obligation") private List obligations; - public String getId() { return id; } + public String getId() { + return id; + } - public List getObligations() { return obligations; } + public List getObligations() { + return obligations; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -89,9 +109,13 @@ public static class Obligation { @JsonProperty("constraint") private List constraints; - public String getAction() { return action; } + public String getAction() { + return action; + } - public List getConstraints() { return constraints; } + public List getConstraints() { + return constraints; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -106,11 +130,17 @@ public static class Constraint { @JsonProperty("rightOperand") private String rightOperand; - public String getLeftOperand() { return leftOperand; } + public String getLeftOperand() { + return leftOperand; + } - public String getOperator() { return operator; } + public String getOperator() { + return operator; + } - public String getRightOperand() { return rightOperand; } + public String getRightOperand() { + return rightOperand; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -125,10 +155,16 @@ public static class DataService { @JsonProperty("endpointURL") private String endpointUrl; - public String getId() { return id; } + public String getId() { + return id; + } - public String getEndpointDescription() { return endpointDescription; } + public String getEndpointDescription() { + return endpointDescription; + } - public String getEndpointUrl() { return endpointUrl; } + public String getEndpointUrl() { + return endpointUrl; + } } } \ No newline at end of file diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java index d6e232129..e04120b20 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java @@ -21,19 +21,13 @@ import jakarta.json.JsonObject; import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; -import org.eclipse.edc.connector.controlplane.transform.odrl.OdrlTransformersFactory; -import org.eclipse.edc.json.JacksonTypeManager; import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.junit.annotations.EndToEndTest; import org.eclipse.edc.junit.testfixtures.TestUtils; -import org.eclipse.edc.participant.spi.ParticipantIdMapper; import org.eclipse.edc.spi.monitor.ConsoleMonitor; -import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.transform.TypeTransformerRegistryImpl; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -46,7 +40,6 @@ import static org.awaitility.Awaitility.await; import static org.eclipse.edc.demo.tests.TestConstants.TEST_POLL_DELAY; import static org.eclipse.edc.demo.tests.TestConstants.TEST_TIMEOUT_DURATION; -import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; /** * This test is designed to run against an MVD deployed in a Kubernetes cluster, with an active ingress controller. @@ -77,27 +70,6 @@ private static RequestSpecification baseRequest() { .when(); } - @BeforeEach - void setup() { - var typeManager = new JacksonTypeManager(); -// transformerRegistry.register(new JsonObjectToCatalogTransformer()); -// transformerRegistry.register(new JsonObjectToDatasetTransformer()); -// transformerRegistry.register(new JsonObjectToDataServiceTransformer()); -// transformerRegistry.register(new JsonObjectToDistributionTransformer()); - transformerRegistry.register(new JsonValueToGenericTypeTransformer(typeManager, JSON_LD)); - OdrlTransformersFactory.jsonObjectToOdrlTransformers(new ParticipantIdMapper() { - @Override - public String toIri(String s) { - return s; - } - - @Override - public String fromIri(String s) { - return s; - } - }).forEach(transformerRegistry::register); - } - @DisplayName("Tests a successful End-to-End contract negotiation and data transfer") @Test void transferData_hasPermission_shouldTransferData() { From ffcf359b5daaec11d4e2fecbbb9928a2e78242e9 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 27 Mar 2026 15:17:32 +0100 Subject: [PATCH 20/22] increase timeout --- .github/workflows/run-e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index fbb60f5f7..6aa711831 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -94,7 +94,7 @@ jobs: kubectl wait -A \ --selector=type=edc-job \ --for=condition=complete job --all \ - --timeout=90s + --timeout=180s - name: "Run E2E Test" run: |- From 97d41a0ddc562eadb1693c47f7f480edb69ca089 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 30 Mar 2026 11:48:22 +0200 Subject: [PATCH 21/22] update readme, Bruno collection --- README.md | 800 +++++------------- .../Download Data from Public API.bru | 32 + .../ControlPlane Management/Get Assets.bru | 37 + .../Get Contract Negotiations.bru | 57 ++ .../Get EDR DataAddress for TransferId.bru | 33 + .../Get cached EDRs.bru | 50 ++ .../Get transfer processes.bru | 32 + .../Initiate Transfer.bru | 47 + .../Initiate negotiation.bru | 62 ++ .../Request Catalog.bru | 62 ++ Requests/ControlPlane Management/folder.bru | 8 + .../DID Document Mgmt API/Add endpoint.bru | 24 + .../DID Document Mgmt API/Delete DID.bru | 22 + .../Get All DID Documents.bru | 21 + .../Get DID Document state.bru | 22 + .../DID Document Mgmt API/Publish DID.bru | 22 + .../DID Document Mgmt API/Query DIDs.bru | 22 + .../DID Document Mgmt API/Un-Publish DID.bru | 22 + .../DID Document Mgmt API/folder.bru | 14 + .../Add KeyPair.bru | 33 + .../Get KeyPair for Participant.bru | 16 + .../Get all KeyPairs.bru | 15 + .../KeyPair Resources Mgmt API/Revoke key.bru | 16 + .../KeyPair Resources Mgmt API/Rotate key.bru | 27 + .../KeyPair Resources Mgmt API/folder.bru | 8 + .../Activate Participant.bru | 38 + .../Create Participant (existing key).bru | 30 + .../Create Participant.bru | 33 + .../Deactivate Participant.bru | 37 + .../Delete Participant.bru | 33 + .../Get Participant By ID.bru | 16 + .../Get all participants.bru | 15 + .../Regenerate Token.bru | 21 + .../Update Roles.bru | 22 + .../Participant Context Mgmt API/folder.bru | 8 + .../Add VerifiableCredential.bru | 61 ++ .../Get All Credentials.bru | 21 + .../Get Credential By ID.bru | 22 + .../Get Credential Reqeusts.bru | 16 + .../Make Credential Request.bru | 42 + .../Query Credentials by Type.bru | 26 + .../VerifiableCredential Mgmt API/folder.bru | 8 + Requests/IdentityHub/folder.bru | 29 + .../Attestations/create Attestation.bru | 292 +++++++ .../Admin API/Attestations/folder.bru | 8 + .../Attestations/query Attestations.bru | 227 +++++ .../create Credential Definition.bru | 386 +++++++++ .../delete Credential Definition By Id.bru | 243 ++++++ .../CredentialDefinitions/folder.bru | 8 + .../CredentialDefinitions/get by ID.bru | 243 ++++++ .../query Credential Definitions.bru | 292 +++++++ .../update Credential Definition.bru | 391 +++++++++ .../Credentials/check Credential Status.bru | 194 +++++ .../Admin API/Credentials/folder.bru | 8 + .../Admin API/Credentials/get Credentials.bru | 299 +++++++ .../Credentials/query Credentials.bru | 310 +++++++ .../Credentials/resume Credential.bru | 198 +++++ .../Credentials/revoke Credential.bru | 194 +++++ .../Credentials/suspend Credential.bru | 194 +++++ .../Query issuance processes.bru | 22 + .../Admin API/IssuanceProcesses/folder.bru | 8 + .../Participants/Create Participant.bru | 221 +++++ .../Admin API/Participants/folder.bru | 8 + .../Participants/get Participant By Id.bru | 214 +++++ .../Participants/query Participants.bru | 286 +++++++ .../Participants/update Participant.bru | 221 +++++ Requests/IssuerService/Admin API/folder.bru | 8 + Requests/IssuerService/folder.bru | 23 + Requests/bruno.json | 9 + Requests/collection.bru | 24 + gradle/libs.versions.toml | 2 + .../application/controlplane-seed.yaml | 4 +- k8s/issuer/application/issuerservice.yaml | 2 +- .../application/controlplane-config.yaml | 2 - .../application/controlplane-seed.yaml | 63 +- launchers/controlplane/build.gradle.kts | 1 + .../org/eclipse/edc/demo/dcp/JwtSigner.java | 158 ---- .../ManufacturerAttestationSource.java | 2 +- resources/data_setup.drawio | 44 - resources/data_setup.png | Bin 97811 -> 0 bytes resources/participants.png | Bin 67271 -> 62469 bytes seed-k8s.sh | 163 ---- .../tests/transfer/TransferEndToEndTest.java | 69 +- .../test/resources/negotiation-request.json | 6 +- .../negotiation-request_invalid.json | 26 + 85 files changed, 6057 insertions(+), 998 deletions(-) create mode 100644 Requests/ControlPlane Management/Download Data from Public API.bru create mode 100644 Requests/ControlPlane Management/Get Assets.bru create mode 100644 Requests/ControlPlane Management/Get Contract Negotiations.bru create mode 100644 Requests/ControlPlane Management/Get EDR DataAddress for TransferId.bru create mode 100644 Requests/ControlPlane Management/Get cached EDRs.bru create mode 100644 Requests/ControlPlane Management/Get transfer processes.bru create mode 100644 Requests/ControlPlane Management/Initiate Transfer.bru create mode 100644 Requests/ControlPlane Management/Initiate negotiation.bru create mode 100644 Requests/ControlPlane Management/Request Catalog.bru create mode 100644 Requests/ControlPlane Management/folder.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Add endpoint.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Delete DID.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Get All DID Documents.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Get DID Document state.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Publish DID.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Query DIDs.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/Un-Publish DID.bru create mode 100644 Requests/IdentityHub/DID Document Mgmt API/folder.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/Add KeyPair.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/Get KeyPair for Participant.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/Get all KeyPairs.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/Revoke key.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/Rotate key.bru create mode 100644 Requests/IdentityHub/KeyPair Resources Mgmt API/folder.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Activate Participant.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Create Participant (existing key).bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Create Participant.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Deactivate Participant.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Delete Participant.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Get Participant By ID.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Get all participants.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Regenerate Token.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/Update Roles.bru create mode 100644 Requests/IdentityHub/Participant Context Mgmt API/folder.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Add VerifiableCredential.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Get All Credentials.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential By ID.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential Reqeusts.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Make Credential Request.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/Query Credentials by Type.bru create mode 100644 Requests/IdentityHub/VerifiableCredential Mgmt API/folder.bru create mode 100644 Requests/IdentityHub/folder.bru create mode 100644 Requests/IssuerService/Admin API/Attestations/create Attestation.bru create mode 100644 Requests/IssuerService/Admin API/Attestations/folder.bru create mode 100644 Requests/IssuerService/Admin API/Attestations/query Attestations.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/create Credential Definition.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/delete Credential Definition By Id.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/folder.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/get by ID.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/query Credential Definitions.bru create mode 100644 Requests/IssuerService/Admin API/CredentialDefinitions/update Credential Definition.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/check Credential Status.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/folder.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/get Credentials.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/query Credentials.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/resume Credential.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/revoke Credential.bru create mode 100644 Requests/IssuerService/Admin API/Credentials/suspend Credential.bru create mode 100644 Requests/IssuerService/Admin API/IssuanceProcesses/Query issuance processes.bru create mode 100644 Requests/IssuerService/Admin API/IssuanceProcesses/folder.bru create mode 100644 Requests/IssuerService/Admin API/Participants/Create Participant.bru create mode 100644 Requests/IssuerService/Admin API/Participants/folder.bru create mode 100644 Requests/IssuerService/Admin API/Participants/get Participant By Id.bru create mode 100644 Requests/IssuerService/Admin API/Participants/query Participants.bru create mode 100644 Requests/IssuerService/Admin API/Participants/update Participant.bru create mode 100644 Requests/IssuerService/Admin API/folder.bru create mode 100644 Requests/IssuerService/folder.bru create mode 100644 Requests/bruno.json create mode 100644 Requests/collection.bru delete mode 100644 launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java delete mode 100644 resources/data_setup.drawio delete mode 100644 resources/data_setup.png delete mode 100755 seed-k8s.sh create mode 100644 tests/end2end/src/test/resources/negotiation-request_invalid.json diff --git a/README.md b/README.md index efa4cacb3..fb69ee998 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,51 @@ # Minimum Viable Dataspace Demo - * [Minimum Viable Dataspace Demo](#minimum-viable-dataspace-demo) - * [1. Introduction](#1-introduction) - * [2. Purpose of this Demo](#2-purpose-of-this-demo) - * [2.1 Which version should I use?](#21-which-version-should-i-use) - * [3. The Scenario](#3-the-scenario) - * [3.1 Participants](#31-participants) - * [3.2 Data setup](#32-data-setup) - * [3.3 Access control](#33-access-control) - * [3.4 DIDs, participant lists and VerifiableCredentials](#34-dids-participant-lists-and-verifiablecredentials) - * [4. Running the demo (inside IntelliJ)](#4-running-the-demo-inside-intellij) - * [4.1 Start NGINX](#41-start-nginx) - * [4.2 Starting the runtimes](#42-starting-the-runtimes) - * [4.3 Seeding the dataspace](#43-seeding-the-dataspace) - * [4.4 Next steps](#44-next-steps) - * [5. Running the Demo (Kubernetes)](#5-running-the-demo-kubernetes) - * [5.1 Build the runtime images](#51-build-the-runtime-images) - * [5.2 Create the K8S cluster](#52-create-the-k8s-cluster) - * [5.3 Seed the dataspace](#53-seed-the-dataspace) - * [5.4 JVM crashes with `SIGILL` on ARM platforms](#54-jvm-crashes-with-sigill-on-arm-platforms) - * [5.5 Debugging MVD in Kubernetes](#55-debugging-mvd-in-kubernetes) - * [6. Differences between Kubernetes and IntelliJ](#6-differences-between-kubernetes-and-intellij) - * [6.1 In-memory databases](#61-in-memory-databases) - * [6.2 Memory-based secret vaults](#62-memory-based-secret-vaults) - * [6.3 Embedded vs Remote STS](#63-embedded-vs-remote-sts) - * [7. Executing REST requests using Postman](#7-executing-rest-requests-using-postman) - * [7.1 Get the catalog](#71-get-the-catalog) - * [7.2 Initiate the contract negotiation](#72-initiate-the-contract-negotiation) - * [7.3 Query negotiation status](#73-query-negotiation-status) - * [7.4 Initiate data transfer](#74-initiate-data-transfer) - * [7.5 Query data transfers](#75-query-data-transfers) - * [7.6 Get EndpointDataReference](#76-get-endpointdatareference) - * [7.7 Get access token for EDR](#77-get-access-token-for-edr) - * [7.8 Fetch data](#78-fetch-data) - * [8. Custom extensions in MVD](#8-custom-extensions-in-mvd) - * [8.1 Catalog Node Resolver](#81-catalog-node-resolver) - * [8.2 Default scope mapping function](#82-default-scope-mapping-function) - * [8.3 Scope extractor for `DataProcessor` credentials](#83-scope-extractor-for-dataprocessor-credentials) - * [8.4 Policy evaluation functions](#84-policy-evaluation-functions) - * [8.4.1 Membership evaluation function](#841-membership-evaluation-function) - * [8.4.2 DataAccessLevel evaluation function](#842-dataaccesslevel-evaluation-function) - * [8.5 Super-user seeding](#85-super-user-seeding) - * [9. Advanced topics](#9-advanced-topics) - * [9.1 Regenerating issuer keys](#91-regenerating-issuer-keys) - * [9.2 Regenerating participant keys](#92-regenerating-participant-keys) - * [9.2.1 IntelliJ deployment:](#921-intellij-deployment) - * [9.2.2 Kubernetes deployment](#922-kubernetes-deployment) - * [10. Other caveats, shortcuts and workarounds](#10-other-caveats-shortcuts-and-workarounds) - * [10.1 In-memory stores in local deployment](#101-in-memory-stores-in-local-deployment) - * [10.2 DID resolution](#102-did-resolution) - * [10.2.1 `did:web` for participants](#1021-didweb-for-participants) - * [10.2.2 `did:web` for the dataspace issuer](#1022-didweb-for-the-dataspace-issuer) - * [10.3 Credential Issuance](#103-credential-issuance) - * [10.4 Default scope-to-criterion transformer](#104-default-scope-to-criterion-transformer) - + * [1. Introduction](#1-introduction) + * [2. Purpose of this Demo](#2-purpose-of-this-demo) + * [2.1 Version stability and backwards compatibility guarantees](#21-version-stability-and-backwards-compatibility-guarantees) + * [2.2 Which version should I use?](#22-which-version-should-i-use) + * [3. The Scenario](#3-the-scenario) + * [3.1 Participants](#31-participants) + * [3.2 Data setup](#32-data-setup) + * [3.3 Access control](#33-access-control) + * [3.4 DIDs, participant lists, and VerifiableCredentials](#34-dids-participant-lists-and-verifiablecredentials) + * [4. Running the Demo (Kubernetes)](#4-running-the-demo-kubernetes) + * [4.1 Build the runtime images](#41-build-the-runtime-images) + * [4.2 Create the K8S cluster](#42-create-the-k8s-cluster) + * [4.3 Deploy the MVD components](#43-deploy-the-mvd-components) + * [4.3 Seed the dataspace](#43-seed-the-dataspace) + * [4.4 Debugging MVD in Kubernetes](#44-debugging-mvd-in-kubernetes) + * [5. Executing REST requests using Bruno](#5-executing-rest-requests-using-bruno) + * [5.1 Get the catalog](#51-get-the-catalog) + * [5.2 Initiate the contract negotiation](#52-initiate-the-contract-negotiation) + * [5.3 Query negotiation status](#53-query-negotiation-status) + * [5.4 Initiate data transfer](#54-initiate-data-transfer) + * [5.5 Query data transfers](#55-query-data-transfers) + * [5.6 Get EndpointDataReference](#56-get-endpointdatareference) + * [5.7 Get access token for EDR](#57-get-access-token-for-edr) + * [5.8 Fetch data](#58-fetch-data) + * [6. Custom extensions in MVD](#6-custom-extensions-in-mvd) + * [7. Advanced topics](#7-advanced-topics) + * [7.2 Regenerating key pairs](#72-regenerating-key-pairs) + * [8. Other caveats, shortcuts, and workarounds](#8-other-caveats-shortcuts-and-workarounds) + * [8.2 DID resolution for participants](#82-did-resolution-for-participants) + * [8.3 Seed Jobs](#83-seed-jobs) ## 1. Introduction -The Decentralized Claims Protocol defines a secure way how to participants in a dataspace can obtain, exchange and +The Decentralized Claims Protocol defines a secure way how two participants in a dataspace can obtain, exchange, and present credential information. In particular, the [DCP specification](https://github.com/eclipse-tractusx/identity-trust) defines the _Presentation Flow_, which is -the process of requesting, presenting and verifying Verifiable Credentials and the _Credential Issuance Flow_, which is +the process of requesting, presenting, and verifying Verifiable Credentials, and the _Credential Issuance Flow_, which +is used to request and issue Verifiable Credentials to a dataspace participant. -So in order to get the most out of this demo, a basic understanding of Verifiable Credentials, Verifiable Presentations, -Decentralized Identifiers (DID) and cryptography is necessary. These concepts will not be explained here further. +So to get the most out of this demo, a basic understanding of Verifiable Credentials, Verifiable Presentations, +Decentralized Identifiers (DID) and general cryptography is necessary. These concepts will not be explained here +further. The Decentralized Claims Protocol was adopted in the Eclipse Dataspace Components project and is currently implemented in modules pertaining to the [Connector](https://github.com/eclipse-edc/connector) as well as @@ -76,7 +54,7 @@ the [IdentityHub](https://github.com/eclipse-edc/IdentityHub). ## 2. Purpose of this Demo This demo is to demonstrate how two dataspace participants can perform a credential exchange prior to a DSP message -exchange, for example requesting a catalog or negotiating a contract. +exchange, for example, requesting a catalog or negotiating a contract. It must be stated in the strongest terms that this is **NOT** a production grade installation, nor should any production-grade developments be based on it. [Shortcuts](#10-other-caveats-shortcuts-and-workarounds) were taken, and @@ -96,12 +74,12 @@ However, all of our development work in MVD targets the `main` branch. In other older releases of MVD. If there is a bug or a new feature either in one of the upstream components or MVD, fixes will _always_ target `main` and will surface in one of the upcoming MVD releases. -This is yet another reason why MVD should _never_ be used in production scenarios! +This is yet another reason why MVD should _never_ be used in production scenarios. Please also note that MVD does not publish any artifacts (Maven, Docker images, ...), adopters have to build from source. -TL;DR - there are none. This is a _sample_ project, not a commercial product. +TL;DR – guarantees? There are none. This is a _sample_ project, not a commercial product. ### 2.2 Which version should I use? @@ -114,7 +92,7 @@ fixes of all upstream components. More conservative developers may fall back to [releases of MVD](https://github.com/eclipse-edc/MinimumViableDataspace/releases) that use release versions of all -upstream components. If this is you, then don't forget to check out the appropriate tag after cloning the repo. +upstream components. If this is you, then remember to check out the appropriate tag after cloning the repo. Either download the ZIP file and use sources therein, or check out the corresponding tag. @@ -122,44 +100,26 @@ An MVD release version is typically created shortly after a release of the upstr ## 3. The Scenario -_In this example, we will see how two companies can share data through federated catalogs using [Management -Domains](https://github.com/eclipse-edc/Connector/blob/main/docs/developer/management-domains/management-domains.md)._ +_In this example, we will see how two companies can share data using DSP and DCP. Each company deploys its own +connector, IdentityHub, and base infrastructure. ### 3.1 Participants There are two fictitious companies, called "Provider Corp" and "Consumer Corp". "Consumer Corp" wants to consume data -from "Provider Corp". Provider Corp has two departments "Q&A" and "Manufacturing". Both are independent and host their -own EDC connectors, named "provider-qna" and "provider-manufacturing". Both are administered individually, but don't -expose their data catalog directly to the internet. - -To make the catalogs available, Provider Corp also hosts a catalog server that is shared between the catalog server, -"provider-qna"" and "provider-manufacturing". +from "Provider Corp". Provider Corp is the data provider, which means, it has to have a catalog of data assets that it +offers to Consumer Corp. -Both Consumer Corp and Provider Corp each operate an IdentityHub. Note that on the provider side, three runtimes -share the same `participantId`, and thus, the same set of credentials, and by extension, the same identity. So the -IdentityHub represents "identities" rather than individual runtimes. A catalog server is a stripped-down EDC runtime, -that only contains modules for servicing catalog requests. +Each company operates its own EDC connector to handle DSP communication, as well as an IdentityHub to handle +VerifiableCredentials and to service DCP interactions. -Consumer Corp has a connector plus its own IdentityHub. +The Consumer Corp connector does not contain any data assets. This is simply to illustrate the current use case, in +practice there is nothing that would keep the Consumer Corp from also offering data to other companies. ![](./resources/participants.png) ### 3.2 Data setup -"provider-qna" and "provider-manufacturing" both have two data assets each, named `"asset-1"` and `"asset-2"` but -neither "provider-qna" nor "provider-manufacturing" expose their catalog endpoint directly to the internet. Instead, the -catalog server (of the Provider Corp) provides a catalog that contains special assets (think: pointers) to both " -provider-qna"'s and "provider-manufacturing"'s connectors, specifically, their DSP endpoints. - -We call this a "root catalog", and the pointers are called "catalog assets". This means, that by resolving the root -catalog, and by following the links therein, "Consumer Corp" can resolve the actual asset from "provider-qna" and -"provider-manufacturing". - -![](./resources/data_setup.png) - -Linked assets, or `CatalogAsset` objects are easily recognizable by the `"isCatalog": true` property. They do not -contain any metadata, only a link to service URL, where the actual asset is available. - +The Provider connector has two data assets, named `"asset-1"` and `"asset-2"`, which reference a demo web API. Note that the consumer connector does not contain any data assets in this scenario. ### 3.3 Access control @@ -168,21 +128,21 @@ In this fictitious dataspace there are two types of VerifiableCredentials: - `MembershipCredential`: contains information about the holder's membership in the dataspace as well as some holder information -- `DataProcessorCredential`: contains the version of the "Data Organization and Processing Edict (DOPE)" the holder has - signed and it attests to the "ability of the holder to process data at a certain level". The following levels exist: - - `"processing"`: means, the holder can process non-sensitive data - - `"sensitive"`: means, the holder has undergone "some very highly secure vetting process" and can process sensitive - data +- `ManufacturerCredential`: attests that the holder is an accredited manufacturer of "parts" and specifies which parts + the holder is allowed to manufacture. This is defined in the `"part_types"` field in the credential subject. The + following variants exist: + - `"part_type": "non_critical"`: means, the holder can manufacture non-critical parts + - `"part_type" : "all"`: means, the holder can manufacture everything including saftey critical parts - The information about the level of data a holder can process is stored in the `credentialSubject` of the - DataProcessorCredential. + The information about the level of the holder is stored in the `credentialSubject` of the + ManufacturerCredential. -Both assets of "provider-qna" and "provider-manufacturing" have some access restrictions on their assets: +Each asset of the provider has access restrictions on it: -- `asset-1`: requires a MembershipCredential to view and a DataProcessorCredential with `"level": "processing"` to +- `asset-1`: requires a MembershipCredential to view and a ManufacturerCredential with `"part_type": "non_critical"` to negotiate a contract and transfer data -- `asset-2`: requires a MembershipCredential to view and a DataProcessorCredential with a `"level": "sensitive"` to - negotiate a contract +- `asset-2`: requires a MembershipCredential to view and a ManufacturerCredential with a `"part_type": "all"` + to negotiate a contract These requirements are formulated as EDC policies: @@ -194,9 +154,9 @@ These requirements are formulated as EDC policies: { "action": "use", "constraint": { - "leftOperand": "DataAccess.level", + "leftOperand": "PartType", "operator": "eq", - "rightOperand": "processing" + "rightOperand": "non_critical" } } ] @@ -207,146 +167,49 @@ These requirements are formulated as EDC policies: In addition, it is a dataspace rule that the `MembershipCredential` must be presented in _every_ DSP request. This credential attests that the holder is a member of the dataspace. -All participants of the dataspace are in possession of the `MembershipCredential` as well as a `DataProcessorCredential` -with level `"processing"`. +All participants of the dataspace are in possession of the `MembershipCredential` as well as a `ManufacturerCredential` +with level `"non_critical"`. -> None possess the `DataProcessorCredential` with level="sensitive". +> None possess the `Manufacturer` with part_type="all". -That means that no contract for `asset-2` can be negotiated by anyone. For the purposes of this demo the -VerifiableCredentials are pre-created and are seeded directly to the participants' credential storage ([no -issuance](#103-no-issuance-yet)) via a dedicated -[extension](launchers/identity-hub/src/main/java/org/eclipse/edc/demo/dcp/ih/IdentityHubExtension.java). +That means that no contract for `asset-2` can be negotiated by anyone. -When the consumer wants to inspect the consolidated catalog (containing assets from both the provider's Q&A and -manufacturing departments), then negotiate a contract for an asset, and then transfer the asset, several credentials -need to be presented: +When the consumer wants to view the catalog, then negotiate a contract for an asset, and then transfer the asset, +several credentials need to be presented: - catalog request: present `MembershipCredential` -- contract negotiation: `MembershipCredential` and `DataProcessorCredential(level=processing)` or - `DataProcessorCredential(level=sensitive)`, respectively +- contract negotiation: `MembershipCredential` and `ManufacturerCredential(part_type=non_critical)` or + `ManufacturerCredential(part_type=all)`, respectively - transfer process: `MembershipCredential` -### 3.4 DIDs, participant lists and VerifiableCredentials - -Participant Identifiers in MVD are Web-DIDs. They are used to identify the holder of a VC, to reference public key -material and to tell the FederatedCatalog Crawlers whom to crawl. DID documents contain important endpoint information, -namely the connector's DSP endpoint and it's CredentialService endpoint. That means that all relevant information about -participants can be gathered simply by resolving and inspecting its DID document. - -One caveat is that with `did:web` DIDs there is a direct coupling between the identifier and the URL. The `did:web:xyz` -identifier directly translates to the URL where the document is resolvable. - -In the context of MVD this means that different DIDs have to be used when running from within IntelliJ versus running in -Kubernetes, since the URLs are different. As a consequence, for every VerifiableCredential there are two variants, one -that contains the "localhost" DID and one that contains the DID with the Kubernetes service URL. Also, the participant -lists are different between those two. - -## 4. Running the demo (inside IntelliJ) - -> Please note that due to the way how Windows handles file paths, running the IntelliJ Run Configs on Windows can -> sometimes cause problems. We recommend either running this from within WSL or on a Linux machine. Alternatively, paths -> could be corrected manually. Running MVD natively on Windows is not supported! - -There are several run configurations for IntelliJ in the `.run/` folder. One each for the consumer and provider -connectors runtimes and IdentityHub runtimes plus one for the provider catalog server, and one named "dataspace". The -latter is a compound run config an brings up all other runtimes together. - -### 4.1 Start NGINX - -The issuer's DID document is hosted on NGINX, so the easiest way of running NGINX is with a docker container: - -```shell -docker run -d --name nginx -p 9876:80 --rm \ - -v "$PWD"/deployment/assets/issuer/nginx.conf:/etc/nginx/nginx.conf:ro \ - -v "$PWD"/deployment/assets/issuer/did.docker.json:/var/www/.well-known/did.json:ro \ - nginx -``` - -To verify that it worked, please execute `curl -X GET http://localhost:9876/.well-known/did.json` and see if it returns -a -DID document as JSON structure: - -```json -{ - "service": [], - "verificationMethod": [ - { - "id": "did:web:localhost%3A9876#key-1", - "type": "JsonWebKey2020", - "controller": "did:web:localhost%3A9876", - "publicKeyMultibase": null, - "publicKeyJwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "Hsq2QXPbbsU7j6JwXstbpxGSgliI04g_fU3z2nwkuVc" - } - } - ], - "authentication": [ - "key-1" - ], - "id": "did:web:localhost%3A9876", - "@context": [ - "https://www.w3.org/ns/did/v1", - { - "@base": "did:web:localhost%3A9876" - } - ] -} -``` - -The port mapping is **important**, because it influences the DID of the issuer: with a host port of -`9876` the issuer DID resolves to `did:web:localhost%3A9876`. Changing the port mapping changes the DID, soif you change -the port mapping, be sure to execute a search-and-replace! - -Naturally, you are free to install NGINX natively on your computer or use any other webserver altogether, but this won't -be supported by us. - -### 4.2 Starting the runtimes - -The connector runtimes contain both the controlplane and the dataplane. Note that in a real-world scenario those would -likely be separate runtimes to be able to scale and deploy them individually. Note also, that the Kubernetes deployment -(next chapter) does indeed run them as separate pods. - -The run configs use the `temurin-22` JDK. If you don't have it installed already, you can choose to install it (IntelliJ -makes this really easy), or to select whatever JDK you have available in each run config. +### 3.4 DIDs, participant lists, and VerifiableCredentials -All run configs take their configuration from `*.env` files which are located in `deployment/assets/env`. +Participant Identifiers in MVD are Web-DIDs. They are used to identify the holder of a VC and to reference public key +material. DID documents contain important endpoint information, namely the connector's DSP endpoint and its +CredentialService endpoint. That means that all relevant information about participants can be gathered simply by +resolving and inspecting its DID document. -### 4.3 Seeding the dataspace +One important caveat is that with `did:web` DIDs there is a direct coupling between the identifier and the URL. The +`did:web:xyz` identifier directly translates to the URL where the document is resolvable. -DID documents are dynamically generated when "seeding" the data, specifically when creating the `ParticipantContext` -objects in IdentityHub. This is automatically being done by a script `seed.sh`. +In the context of MVD this means that DIDs have to be crafted such that they reference the internal Kubernetes service +URL of the DID endpoint. Since IdentityHub is used to host DID documents, the Kubernetes service URL is the URL of the +IdentityHub's web endpoint, for example `did:web:identityhub.consumer.svc.cluster.local%3A7083:consumer`, which would +convert into `http://identityhub.consumer.svc.cluster.local:7083/consumer/.well-known/did.json`. -After executing the `dataspace` run config in Intellij, be sure to **execute the `seed.sh` script after all the runtimes -have started**. Omitting to do so will leave the dataspace in an uninitialized state and cause all -connector-to-connector communication to fail. +## 4. Running the Demo (Kubernetes) -### 4.4 Next steps - -All REST requests made from the script are available in the [Postman -collection](./deployment/postman/MVD.postman_collection.json). With the [HTTP -Client](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) and [Import from Postman -Collections](https://plugins.jetbrains.com/plugin/22438-import-from-postman-collections) plugins, the Postman collection -can be imported and then executed by means of the [environment file](./deployment/postman/http-client.env.json), -selecting the "Local" environment. - -Please read [chapter 7](#7-executing-rest-requests-using-postman) for details. - -## 5. Running the Demo (Kubernetes) - -For this section a basic understanding of Kubernetes, Docker, Gradle and Terraform is required. It is assumed that the -following tools are installed and readily available: +The demo is intended to be run in Kubernetes, so for this section a basic understanding of Kubernetes, Docker, and +Gradle +is required. It is assumed that the following tools are installed and readily available: - Docker -- KinD (other cluster engines may work as well - not tested!) -- Terraform +- KinD (other cluster engines may work as well – not tested!) +- Helm (used to install the Traefik Gateway Controller) - JDK 17+ - Git - a POSIX compliant shell -- Postman (to comfortably execute REST requests) -- `openssl`, optional, but required to [regenerate keys](#91-regenerating-issuer-keys) -- `newman` (to run Postman collections from the command line) +- Bruno (to comfortably execute REST requests) - not needed, but recommended: Kubernetes monitoring tools like K9s All commands are executed from the **repository's root folder** unless stated otherwise via `cd` commands. @@ -354,207 +217,162 @@ All commands are executed from the **repository's root folder** unless stated ot > Since this is not a production deployment, all applications are deployed _in the same cluster_ and in the same > namespace, plainly for the sake of simplicity. -### 5.1 Build the runtime images +### 4.1 Build the runtime images ```shell ./gradlew build -./gradlew -Ppersistence=true dockerize +./gradlew dockerize ``` -this builds the runtime images and creates the following docker images: `controlplane:latest`, `dataplane:latest`, -`catalog-server:latest` and `identity-hub:latest` in the local docker image cache. Note the `-Ppersistence` flag which -puts the HashiCorp Vault module and PostgreSQL persistence modules on the runtime classpath. - -> This demo will not work properly, if the `-Ppersistence=true` flag is omitted! +This builds the runtime images and creates the following docker images: `ghcr.io/eclipse-edc/mvd/controlplane:latest`, +`ghcr.io/eclipse-edc/mvd/dataplane:latest`, `ghcr.io/eclipse-edc/mvd/issuerservice:latest` and +`ghcr.io/eclipse-edc/mvd/identity-hub:latest` in the local docker image cache. -PostgreSQL and Hashicorp Vault obviously require additional configuration, which is handled by the Terraform scripts. +PostgreSQL and Hashicorp Vault obviously require additional configuration, which is handled by the Kubernetes manifests +via batch jobs. -### 5.2 Create the K8S cluster +### 4.2 Create the K8S cluster After the runtime images are built, we bring up and configure the Kubernetes cluster. We are using KinD here, but this -should work similarly well on other cluster runtimes, such as MicroK8s, K3s or Minikube. Please refer to the respective +should work similarly well on other cluster runtimes, such as MicroK8s, K3s, or Minikube. Please refer to the respective documentation for more information. ```shell # Create the cluster -kind create cluster -n mvd --config deployment/kind.config.yaml +kind create cluster -n mvd -# Load docker images into KinD -kind load docker-image controlplane:latest dataplane:latest identity-hub:latest catalog-server:latest issuerservice:latest -n mvd +``` -# Deploy an NGINX ingress -kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml +### 4.3 Deploy the MVD components +The following commands deploy the MVD components to the cluster. -# Wait for the ingress controller to become available -kubectl wait --namespace ingress-nginx \ - --for=condition=ready pod \ - --selector=app.kubernetes.io/component=controller \ +```shell +# Load docker images into KinD +kind load docker-image \ + ghcr.io/eclipse-edc/mvd/controlplane:latest \ + ghcr.io/eclipse-edc/mvd/dataplane:latest \ + ghcr.io/eclipse-edc/mvd/identity-hub:latest \ + ghcr.io/eclipse-edc/mvd/issuerservice:latest -n mvd + +# install Traefik +helm repo add traefik https://traefik.github.io/charts +helm repo update +helm upgrade --install --namespace traefik traefik traefik/traefik --create-namespace -f values.yaml + +# Wait for traefik to be ready +kubectl rollout status deployment/traefik -n traefik --timeout=120s + +# install Gateway API CRDs +kubectl apply --server-side --force-conflicts -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/experimental-install.yaml + +# install MVD using Kustomize +kubectl apply -k k8s + +# wait for all init jobs to have completed +kubectl wait -A \ + --selector=type=edc-job \ + --for=condition=complete job --all \ --timeout=90s - -# Deploy the dataspace, type 'yes' when prompted -cd deployment -terraform init -terraform apply + +# forward port 80 (might require sudo) +kubectl port-forward svc/traefik 80:80 -n traefik ``` -Once Terraform has completed the deployment, type `kubectl get pods` and verify the output: +Once all jobs have finished, type `kubectl get pods -A` and verify the output: ```shell -❯ kubectl get pods --namespace mvd +❯ kubectl get pods -A NAME READY STATUS RESTARTS AGE -consumer-controlplane-5854f6f4d7-pk4lm 1/1 Running 0 24s -consumer-dataplane-64c59668fb-w66vz 1/1 Running 0 17s -consumer-identityhub-57465876c5-9hdhj 1/1 Running 0 24s -consumer-postgres-6978d86b59-8zbps 1/1 Running 0 40s -consumer-vault-0 1/1 Running 0 37s -provider-catalog-server-7f78cf6875-bxc5p 1/1 Running 0 24s -provider-identityhub-f9d8d4446-nz7k7 1/1 Running 0 24s -provider-manufacturing-controlplane-d74946b69-rdqnz 1/1 Running 0 24s -provider-manufacturing-dataplane-546956b4f8-hkx85 1/1 Running 0 17s -provider-postgres-75d64bb9fc-drf84 1/1 Running 0 40s -provider-qna-controlplane-6cd65bf6f7-fpt7h 1/1 Running 0 24s -provider-qna-dataplane-5dc5fc4c7d-k4qh4 1/1 Running 0 17s -provider-vault-0 1/1 Running 0 36s +consumer controlplane-6585c89dcb-zzlvw 1/1 Running 0 2d16h +consumer identityhub-67f54b84c8-7847b 1/1 Running 0 2d16h +consumer postgres-84bf65d6fb-f65fr 1/1 Running 0 2d16h +consumer vault-6b6d47654d-bjvxs 1/1 Running 0 2d16h +issuer issuerservice-6f6f8d5f4d-z9lxv 1/1 Running 0 2d16h +issuer postgres-54b66fb487-g2d8n 1/1 Running 0 2d16h +issuer vault-6b6d47654d-z94zn 1/1 Running 0 2d16h +mvd-common keycloak-787fbf7dbc-rr2rq 1/1 Running 0 2d16h +mvd-common postgres-74bf65fcbd-7j9hw 1/1 Running 0 2d16h +provider controlplane-6585c89dcb-dtbwj 1/1 Running 0 2d16h +provider dataplane-6b46bdbbf-hsrzd 1/1 Running 0 2d16h +provider identityhub-67f54b84c8-gbtq8 1/1 Running 0 2d16h +provider postgres-84bf65d6fb-bl6ft 1/1 Running 0 2d16h +provider vault-6b6d47654d-cf8c2 1/1 Running 0 2d16h +traefik traefik-696d96b7bb-pprmq 1/1 Running 1 (2d18h ago) 3d15h ``` -The consumer company has a controlplane, a dataplane, an IdentityHub, a postgres database and a vault to store secrets. -The provider company has a catalog server, a "provider-qna" and a "provider-manufacturing" controlplane/dataplane combo -plus an IdentityHub, a postgres database and a vault. - -It is possible that pods need to restart a number of time before the cluster becomes stable. This is normal and -expected. If pods _don't_ come up after a reasonable amount of time, it is time to look at the logs and investigate. - -Remote Debugging is possible, but Kubernetes port-forwards are necessary. - -### 5.3 Seed the dataspace +_seed job pods and some unrelated pods have been omitted for brevity_ -Once all the deployments are up-and-running, the seed script needs to be executed which should produce command line -output similar to this: - -```shell -./seed-k8s.sh +The consumer company has a controlplane, an IdentityHub, a postgres database, and a vault to store secrets. +The provider company has a control plane, a dataplane, plus an IdentityHub, a postgres database, and a vault. +In addition, there is the Issuer service, which is responsible for issuing Verifiable Credentials. -Seed data to "provider-qna" and "provider-manufacturing" -(node:545000) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. -(Use `node --trace-deprecation ...` to show where the warning was created) -(node:545154) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. -(Use `node --trace-deprecation ...` to show where the warning was created) - - -Create linked assets on the Catalog Server -(node:545270) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. -(Use `node --trace-deprecation ...` to show where the warning was created) - - -Create consumer participant -ZGlkOndlYjphbGljZS1pZGVudGl0eWh1YiUzQTcwODM6YWxpY2U=.KPHR02XRnn+uT7vrpCIu8jJUADTBHKrterGq0PZTRJgzbzvgCXINcMWM3WBraG0aV/NxdJdl3RH3cqgyt+b5Lg== - -Create provider participant -ZGlkOndlYjpib2ItaWRlbnRpdHlodWIlM0E3MDgzOmJvYg==.wBgVb44W6oi3lXlmeYsH6Xt3FAVO1g295W734jivUo5PKop6fpFsdXO4vC9D4I0WvqfB/cARJ+FVjjyFSIewew==% -``` - -_the `node` warnings are harmless and can be ignored_ +It is possible that pods need to restart a number of time before the cluster becomes stable. This is normal and +expected. If pods _don't_ come up after a reasonable amount of time, it is time to look at the logs and investigate. -> Failing to run the seed script will leave the dataspace in an uninitialized state and cause all connector-to-connector -> communication to fail. -> +Remote Debugging is possible, but Kubernetes port-forwards of port 1044 are necessary. -### 5.4 JVM crashes with `SIGILL` on ARM platforms +> Please note that the Keycloak service is deployed _only once_ and is shared by all participants. We are aware that +> this +> is bad practice and should not be done in production, but Keycloak is a very heavy service that takes a long time to +> start up, and we want to keep the demo simple. -We have noticed, that the JVM inside the Docker container sometimes crashes with a `SIGILL` signal right -away without even starting the runtime. So far we've only seen this on ARM platforms such as Apple Silicon. The `UseSVE` -option seems to [mitigate this](https://github.com/corretto/corretto-21/issues/85). If you are affected by this, please -try enabling the `useSVE` switch: +### 4.3 Seed the dataspace -``` -terraform apply -var="useSVE=true" -``` +Once all pods are up and running, and all seed jobs have completed, all necessary demo data is already in place, no need +to execute scripts or manually invoke the REST API. -This will add the `-XX:UseSVE=0` switch to the `JAVA_TOOL_OPTIONS` in all runtimes, enabling the Scalable Vector -Extensions that are available on ARM processors. Alternatively, you can also set the `useSVE = true` variable in a -`*.tfvars` file, cf. [documentation](https://developer.hashicorp.com/terraform/language/values/variables). +This includes: -_Important note: on non-ARM platforms, the `-XX:UseSVE=0` VM option is not recognized and will crash the JVM!_ +- vault bootstrap: sets up the vault with the necessary secrets and some configuration +- consumer identityhub: creates a user account for the consumer and requests verifiable credentials +- provider identityhub: creates a user account for the provider +- consumer controlplane: creates Common Expression Language (CEL) entries to be able to interpret the provider's policy + constraints +- provider controlplane: creates assets, policies, contract definitions and registers a dataplane instance -### 5.5 Debugging MVD in Kubernetes +### 4.4 Debugging MVD in Kubernetes -All of MVD's runtime images come with remote JVM debugging enabled by default. This is configured by setting an +All of MVD's runtime images come with remote JVM debugging enabled by default. This is already configured by setting an environment variable ``` JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" ``` -All runtimes use port **1044** for debugging, unless configured otherwise in terraform. The only thing left to do for -you is to create a Kubernetes port-forwarding: +All runtimes use port **1044** for debugging, unless configured otherwise in their respective Kubernetes ConfigMap. The +only thing left to do for you is to create a Kubernetes port-forwarding: ```shell -kubectl port-forward -n mvd service/consumer-controlplane 1044:1044 +kubectl port-forward -n consumer service/consumer-controlplane 1044:1044 ``` -This assumes the default Kubernetes namespace `mvd`. Note that the port-forward targets a `service` to have it +This assumes the default Kubernetes namespace `consumer`. Note that the port-forward targets a `service` to have it consistent across pod restarts, but targeting a specific pod is also possible. Please refer to the official documentation for details. The host port (the value after the `:`) is completely arbitrary, and should be altered if multiple runtimes are debugged in parallel. -When creating a "Remote JVM Debug" run configuration in IntelliJ it is important to select the appropriate module +When creating a "Remote JVM Debug" run configuration in IntelliJ, it is important to select the correct module classpath. Those are generally located in the `launchers/` directory. Please also refer to the [official IntelliJ tutorial](https://www.jetbrains.com/help/idea/tutorial-remote-debug.html) on how to do remote debugging. -## 6. Differences between Kubernetes and IntelliJ - -The focus with the Kubernetes deployment is to achieve a "one-click-deployment" (don't count them, it's more than 1) -with minimum hassle for people who don't necessarily have developer tools installed on their computers. Conversely, the -deployment with IntelliJ is intended to give developers an easy way to debug and trace the code base, and to extend and -play with MVD without having to do the entire rebuild-docker-image-redeploy loop every time. Also, debugging is much -easier. - -However, to keep the IntelliJ setup as simple as possible, a few shortcuts were taken: - -### 6.1 In-memory databases - -No persistent storage is available, because that would have meant manually setting up and -populating several PostgreSQL databases. Everytime a runtime (re-)starts, it starts with a clean slate. This can cause -some inconsistencies, e.g. when consumer and provider have negotiated a contract, and the provider restarts, the -contract will be missing from its database. Keep that in mind. It is recommended to always restart the _entire_ -dataspace with the included composite run config. - -### 6.2 Memory-based secret vaults +## 5. Executing REST requests using Bruno -This is the big one. Since the memory-vault is compiled into the runtime, components of one -participant (e.g. controlplane and identityhub) _do not share_ vault secrets, because their vaults are different. Thus, -all secrets that need to be accessed by multiple components must be pre-populated. - -### 6.3 Embedded vs Remote STS - -While in the Kubernetes deployment the SecureTokenService (S)S is a stand-alone component, in the IntelliJ -deployment it is embedded into the controlplane. The reason for this is, that during seeding a participant context and -an STS Account is created. This includes a (generated) client secret, that gets stored in the vault. - -In the IntelliJ case that vault is purely in-memory and is isolated in IdentityHub, with no way to access it from the -connector's controlplane. So the connector's controlplane and IdentityHub physically cannot share any secrets. To -overcome this, STS is simply embedded in the controlplane directly. - -In the Kubernetes deployment this limitation goes away, because a dedicated vault service (HashiCorp Vault) is used, -which is accessible from either component. - -## 7. Executing REST requests using Postman - -This demo comes with a Postman collection located in `deployment/postman`. Be aware that the collection has different +This demo comes with a Bruno collection located in `Requests`. Be aware that the collection has different sets of variables in different environments, "MVD local development" and "MVD K8S". These are located in the same -directory and must be imported into Postman too. +directory and must be imported into Bruno too. -The collection itself is pretty self-explanatory, it allows you to request a catalog, perform a contract negotiation and +The collection itself is pretty self-explanatory, it allows you to request a catalog, perform a contract negotiation, +and execute a data transfer. The following sequence must be observed: -### 7.1 Get the catalog +### 5.1 Get the catalog to get the dataspace catalog across all participants, execute `ControlPlane Management/Get Cached Catalog`. Note that it takes a few seconds for the consumer connector to collect all entries. Watch out for a dataset entry named `asset-1` @@ -608,11 +426,11 @@ service entry should be: Important: copy the `@id` value of the `odrl:hasPolicy`, we'll need that to initiate the negotiation! -### 7.2 Initiate the contract negotiation +### 5.2 Initiate the contract negotiation From the previous step we have the `odrl:hasPolicy.@id` value, that should look something like `bWVtYmVyLWFuZC1wY2YtZGVm:YXNzZXQtMQ==:MThhNTgwMzEtNjE3Zi00N2U2LWFlNjMtMTlkZmZlMjA5NDE4`. This value must now be copied -into the `policy.@id` field of the `ControlPlane Management/Initiate Negotiation` request of the Postman collection: +into the `policy.@id` field of the `ControlPlane Management/Initiate Negotiation` request of the Bruno collection: ```json //... @@ -627,7 +445,7 @@ into the `policy.@id` field of the `ControlPlane Management/Initiate Negotiation You will receive a response immediately, but that only means that the request has been received. In order to get the current status of the negotiation, we'll have to inquire periodically. -### 7.3 Query negotiation status +### 5.3 Query negotiation status With the `ControlPlane Management/Get Contract Negotiations` request we can periodically query the status of all our contract negotiations. Once the state shows `FINALIZED`, we copy the value of the `contractAgreementId`: @@ -641,7 +459,7 @@ contract negotiations. Once the state shows `FINALIZED`, we copy the value of th } ``` -### 7.4 Initiate data transfer +### 5.4 Initiate data transfer From the previous step we have the `contractAgreementId` value `3fb08a81-62b4-46fb-9a40-c574ec437759`. In the `ControlPlane Management/Initiate Transfer` request we will paste that into the `contractId` field: @@ -658,7 +476,7 @@ From the previous step we have the `contractAgreementId` value `3fb08a81-62b4-46 } ``` -### 7.5 Query data transfers +### 5.5 Query data transfers Like with contract negotiations, data transfers are asynchronous processes so we need to periodically query their status using the `ControlPlane Management/Get transfer processes` request. Once we find a `"state": "STARTED"` field in the @@ -669,7 +487,7 @@ dataplane's public endpoint, as we would query any other REST API. However, an a the request. This access token is provided to the consumer in the form of an EndpointDataReference (EDR). We must thus query the consumer's EDR endpoint to obtain the token. -### 7.6 Get EndpointDataReference +### 5.6 Get EndpointDataReference Using the `ControlPlane Management/Get Cached EDRs` request, we fetch the EDR and note down the value of the `@id` field, for example `392d1767-e546-4b54-ab6e-6fb20a3dc12a`. This should be identical to the value of the @@ -677,7 +495,7 @@ field, for example `392d1767-e546-4b54-ab6e-6fb20a3dc12a`. This should be identi With that value, we can obtain the access token for this particular EDR. -### 7.7 Get access token for EDR +### 5.7 Get access token for EDR In the `ControlPlane Management/Get EDR DataAddress for TransferId` request we have to paste the `transferProcessId` value from the previous step in the URL path, for example: @@ -702,7 +520,7 @@ authorization token: Note that the token was abbreviated for legibility. -### 7.8 Fetch data +### 5.8 Fetch data Using the endpoint and the authorization token from the previous step, we can then download data using the `ControlPlane Management/Download Data from Public API` request. To do that, the token must be copied into the request's @@ -712,231 +530,61 @@ Important: do not prepend a `bearer` prefix! This will return some dummy JSON data. -## 8. Custom extensions in MVD +## 6. Custom extensions in MVD -EDC is not a turn-key application, rather it is a set of modules, that have to be configured, customized and extended to -fit the needs of any particular dataspace. +EDC is not a turn-key application, rather it is a set of modules that may be configured, customized, and extended +to fit the needs of any particular dataspace. -For our demo dataspace there are a several extensions that are required. These can generally be found in the +For our demo dataspace there are two extensions that are required. These can generally be found in the `extensions/` directory, or directly in the `src/main/java` folder of the launcher module. -### 8.1 Catalog Node Resolver - -Out-of-the-box the FederatedCatalog comes with an in-memory implementation of the `TargetNodeDirectory`. A -`TargetNodeDirectory` is a high-level list of participants of the dataspace, a "phone book" if you will. In MVD that -phone book is constituted by a hard-coded [file](./deployment/assets/participants), where every participant is listed -with their DID. - -To keep things simple, MVD comes with a custom -[implementation](extensions/catalog-node-resolver/src/main/java/org/eclipse/edc/demo/participants/resolver/LazyLoadNodeDirectory.java) -for those participant directory files. - -Everything we need such as DSP URLs, public keys, CredentialService URLs is resolved from the DID document. - -### 8.2 Default scope mapping function - -As per our [dataspace rules](#33-access-control), every DSP request has to be secured by presenting the Membership -credential, even the Catalog request. In detail, this means, that every DSP request that the consumer sends, must carry -a token in the Authorization header, which authorizes the verifier to obtain the MembershipCredential from the -consumer's IdentityHub. - -We achieve this by intercepting the DSP request and adding the correct scope - here: -`"org.eclipse.dspace.dcp.vc.type:MembershipCredential:read"` - to the request builder. Technically, this is achieved by -registering a `postValidator` function for the relevant policy scopes, check out the -[DcpPatchExtension.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DcpPatchExtension.java) class. - -### 8.3 Scope extractor for `DataProcessor` credentials - -When the consumer wants to negotiate a contract for an offer, that has a `DataAccess.level` constraint, it must add the -relevant scope string to the access token upon DSP egress. A policy, that requires the consumer to present a -`DataProcessorCredential`, where the access level is `processing` would look like this: - -```json -{ - "@type": "Set", - "obligation": [ - { - "action": "use", - "constraint": { - "leftOperand": "DataAccess.level", - "operator": "eq", - "rightOperand": "processing" - } - } - ] -} -``` - -The -[DataAccessCredentialScopeExtractor.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/core/DataAccessCredentialScopeExtractor.java) -class would convert this into a scope string `org.eclipse.dspace.dcp.vc.type:DataProcessorCredential:read` and add it to the -consumer's access token. - -### 8.4 Policy evaluation functions - -Being able to express a constraint in ODRL gets us only halfway there, we also need some code to evaluate that -expression. In EDC, we do this by registering policy evaluation functions with the policy engine. - -Since our dataspace defines two credential types, which can be used in policies, we also need two evaluation functions. - -#### 8.4.1 Membership evaluation function - -This function is used to evaluate Membership constraints in policies by asserting that the Membership credential is -present, is not expired and the membership is in force. This is implemented in the -[MembershipCredentialEvaluationFunction.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/MembershipCredentialEvaluationFunction.java). - -#### 8.4.2 DataAccessLevel evaluation function - -Similarly, to evaluate `DataAccess.level` constraints, there is a -[DataAccessLevelFunction.java](extensions/dcp-impl/src/main/java/org/eclipse/edc/demo/dcp/policy/DataAccessLevelFunction.java) -class, that asserts that a DataProcessor credential is present, and that the level is appropriate. Note that to do that, -the function implementation needs to have knowledge about the shape and schema of the `credentialSubject` of the -DataProcessor VC. - -> Hint: all credentials, where the `credentialSubject` has the same shape/schema can be evaluated by the same function! - -### 8.5 Super-user seeding - -IdentityHub's [Identity -API](https://github.com/eclipse-edc/IdentityHub/blob/main/docs/developer/architecture/identityhub-apis.md#identity-api) -is secured with a basic RBAC system. For this, there is a special role called the `"super-user"`. Creating participants -must be done using this role, but unless this role exists, we can't create any participants... so we are facing a bit of -a chicken-and-egg problem. - -This is why seeding the "super-user" is done directly from code using the -[ParticipantContextSeedExtension.java](extensions/superuser-seed/src/main/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtension.java). +These are: -If a "super-user" does not already exist, one is created in the database using defaults. Feel free to override the -defaults and customize your "super-user" and find out what breaks :) +- `extensions/data-plane-public-api-v2`: this is a naïve implementation of an HTTP data plane. In earlier versions of + EDC, this was included in the core EDC code base, but has since been deprecated. +- `extensions/data-plane-registration`: until the full functionality of the Data Plane Signaling specification is + implemented and stable, we'll use this workaround to register data planes with the control plane. Omitting this will + cause the catalog to be empty! +- `launchers/issuerservice`: contains code to be able to generate/issue ManufacturerCredentials and + MembershipCredentials -> NB: doing this in anything but a demo installation is **not** recommended, as it poses significant security risks! +## 7. Advanced topics -## 9. Advanced topics +### 7.2 Regenerating key pairs -### 9.1 Regenerating issuer keys - -The dataspace issuer is the authoritative entity that can issue Verifiable Credentials to participants. For that, two -things are needed: a private/public key pair to sign credentials, and a DID document for verifiers to obtain the -dataspace issuer's public key. - -Consequently, when the dataspace issuer's keys should be updated, these aforementioned places are relevant. - -The first step is to create a new key pair: - -```shell -openssl genpkey -algorithm ed25519 -out deployment/assets/issuer_private.pem -openssl pkey -in assets/issuer_private.pem -pubout -out assets/issuer_public.pem -``` - -These puts a new key pair in `deployment/assets/`. Note that the path is arbitrary, but needs to be consistent with -subsequent steps. -Next, we need to re-sign the participants' credentials, update the database seed data and update the issuer's DID -document. - -There is no easy or convenient way to do this natively on the command line, so we created a test -named [JwtSigner.java](launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java) that does all that. -Simply executing the test performs all these steps, updates files etc. - -The only thing left to do is to clean-rebuild-restart the applications (IntelliJ) or rebuild and redeploy (Kubernetes). - -> We strongly encourage readers to closely inspect the `JwtSigner` code, because it shows how key conversion, document -> handling etc. can be done in EDC! - -### 9.2 Regenerating participant keys - -#### 9.2.1 IntelliJ deployment: - -keys must be seeded at startup time (due to [this limitation](#62-memory-based-secret-vaults)). -In addition, if consumer and provider have the same key, that makes things a bit easier, because it removes the need to -seed the keys via config or commandline argument. That said, the process is similar to the dataspace issuer: - -```shell -openssl genpkey -algorithm ed25519 -out deployment/assets/consumer_private.pem -openssl pkey -in deployment/assets/consumer_private.pem -pubout -out deployment/assets/consumer_public.pem - -# use the same key for provider: -cp deployment/assets/consumer_private.pem deployment/assets/provider_private.pem -cp deployment/assets/consumer_public.pem deployment/assets/provider_public.pem -``` - -Now comes the hacky part, reader discretion is advised. -In [SecretsExtension.java](extensions/did-example-resolver/src/main/java/org/eclipse/edc/iam/identitytrust/core/SecretsExtension.java) -replace the String block for the private and public key with the contents of the newly created `*.pem` files. - -Clean-rebuild-restart the applications. Don't forget to [seed](#43-seeding-the-dataspace). Done. - -#### 9.2.2 Kubernetes deployment - -Here, participant keys are dynamically generated by IdentityHub, so there is no need to pre-generate them. In fact, -everytime the dataspace is re-deployed and the [seed script](#53-seed-the-dataspace) is executed, a new key pair is -generated for each participant. +Participant keys are dynamically generated by IdentityHub, so there is no need to pre-generate them. In fact, +everytime the dataspace is re-deployed and the seed jobs are executed, a new key pair is generated for each participant. To be extra-precise, the keys are regenerated when a new `ParticipantContext` is created. -## 10. Other caveats, shortcuts and workarounds +At runtime, a participant's key pair(s) can be regenerated and revoked using +IdentityHub's [IdentityAPI](https://eclipse-edc.github.io/IdentityHub/openapi/identity-api/#/). + +## 8. Other caveats, shortcuts, and workarounds It must be emphasized that this is a **DEMO**, it does not come with any guarantee w.r.t. operational readiness and comes with a few significant shortcuts affecting security amongst other things, for the sake of simplicity. These are: -### 10.1 In-memory stores in local deployment - -When running the MVD from IntelliJ, the runtimes exclusively use in-memory stores and in-memory vaults. We opted for -this to avoid having to either provide (and maintain) a docker-compose file for those services, or to put users through -an arduous amount of setup and configuration. - -The Kubernetes deployment uses both persistent storage (PostgreSQL) and secure vaults (Hashicorp Vault). - -### 10.2 DID resolution +### 8.2 DID resolution for participants -#### 10.2.1 `did:web` for participants - -Participants hosts their DIDs in their IdentityHubs, which means, that the HTTP-URL that the DID maps to must be +Participants host their DIDs in their IdentityHubs, which means that the HTTP-URL that the DID maps to must be accessible for all other participants. For example, every participant pod in the cluster must be able to resolve a DID from every other participant. For access to pods from outside the cluster we would be using an ingress controller, but -then the other pods in the cluster cannot access it, due to missing DNS entries. That means, that the DID cannot use the -_ingress URL_, but must use the _service's_ URL. A service in turn is not accessible from outside the cluster, so DIDs -are only resolvable from _inside_ the cluster. Unfortunately, there is no way around this, unless we put DIDs on a -publicly resolvable CDN or webserver. - -#### 10.2.2 `did:web` for the dataspace issuer +then the other pods in the cluster cannot access it, due to missing DNS entries. That means that the DID cannot use the +_gateway/httproute URL_, but must use the _service's_ URL. A service in turn is not accessible from outside the cluster, +so DIDs are only resolvable from _inside_ the cluster. Unfortunately, there is no way around this unless we put DIDs on +a publicly resolvable CDN or webserver. -The "dataspace issuer" does not exist as participant yet, so instead of deploying a fake IdentityHub, we opted for -simply hosting the dataspace issuer's DID as static file with NGINX. +### 8.3 Seed Jobs -### 10.3 Credential Issuance +When deploying the dataspace for the first time, the seed jobs are executed putting required data into the database of +each component. No special action is required, they run automatically. -Even though the DCP Credential Issuance Protocol is now supported, credentials are pre-generated manually and -distributed to the participants during deployment. Credentials are put into the stores by an extension called -`IdentityHubExtension.java` and are **different** for local deployments and Kubernetes deployments. +Since they perform several consecutive REST requests, they might get quite daunting to look at and hard to debug. +Essentially, they use each component's internal administration API to perform the necessary actions. -The [JwtSigner.java](launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java) test class can be -used to re-generate and sign all credentials. - -Additional credentials can be requested from the dataspace issuer using the `MVD/IdentityHub/Make Credential Request` -Postman request or by executing on a shell: +To re-run a seed job, the simplest way is to delete it and re-deploy: ```shell -curl --location 'http://localhost/consumer/cs//api/identity/v1alpha/participants/ZGlkOndlYjpjb25zdW1lci1pZGVudGl0eWh1YiUzQTcwODM6Y29uc3VtZXI=/credentials/request' \ ---header 'Content-Type: application/json' \ ---header 'X-Api-Key: c3VwZXItdXNlcg==.c3VwZXItc2VjcmV0LWtleQo=' \ ---data '{ - "issuerDid": "did:web:dataspace-issuer-service%3A10016:issuer", - "holderPid": "credential-request-1", - "credentials": [{ - "format": "VC1_0_JWT", - "credentialType": "DemoCredential" - }] -}' -``` - -Note that the example assumes a Kubernetes deployment. This will cause the `dataspace-issuer-service` to generate and -deliver a credential of type `DemoCredential` to the consumer's IdentityHub. - -### 10.4 Default scope-to-criterion transformer - -When IdentityHub receives a Presentation query, that carries an access token, it must be able to convert a scope string -into a filter expression, for example `org.eclipse.dspace.dcp.vc.type:DataProcessorCredential:read` is converted into -`verifiableCredential.credential.type = DataProcessorCredential`. This filter expression is then used by IdentityHub to -query for `DataProcessorCredentials` in the database. - -The MVD uses the default `EdcScopeToCriterionTransformer` to achieve this. It is recommended to implement a custom -`ScopeToCriterionTransformer` for an actual production scenario. +kubectl delete -f k8s/provider/application/controlplane-seed.yaml +kubectl apply -f k8s/provider/application/controlplane-seed.yaml +``` \ No newline at end of file diff --git a/Requests/ControlPlane Management/Download Data from Public API.bru b/Requests/ControlPlane Management/Download Data from Public API.bru new file mode 100644 index 000000000..0222ce06c --- /dev/null +++ b/Requests/ControlPlane Management/Download Data from Public API.bru @@ -0,0 +1,32 @@ +meta { + name: Download Data from Public API + type: http + seq: 9 +} + +get { + url: {{PROVIDER_PUBLIC_URL}}/api/public + body: none + auth: none +} + +headers { + Authorization: {{AUTHORIZATION}} +} + +script:pre-request { + if(!(bru.getVar("AUTHORIZATION") !== undefined && bru.getVar("AUTHORIZATION") !== null)){ + throw new Error(' The authorization token is not yet available, please execute request "Get EDR DataAddress for TransferId" first!'); + } +} + +script:post-response { + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Get Assets.bru b/Requests/ControlPlane Management/Get Assets.bru new file mode 100644 index 000000000..b3e5760c2 --- /dev/null +++ b/Requests/ControlPlane Management/Get Assets.bru @@ -0,0 +1,37 @@ +meta { + name: Get Assets + type: http + seq: 1 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/assets/request + body: json + auth: inherit +} + +body:json { + { + "@type": "QuerySpec" + } +} + +body:text { + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "QuerySpec" + } +} + +script:post-response { + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Get Contract Negotiations.bru b/Requests/ControlPlane Management/Get Contract Negotiations.bru new file mode 100644 index 000000000..0749fb67d --- /dev/null +++ b/Requests/ControlPlane Management/Get Contract Negotiations.bru @@ -0,0 +1,57 @@ +meta { + name: Get Contract Negotiations + type: http + seq: 4 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/contractnegotiations/request + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +body:text { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +script:post-response { + // get the contact agreement id and save it as an environment variable + if(res.getStatus() < 300 && res.getStatus() >= 200 && res.getBody().length > 0){ + var find_negotiation; + if (bru.getVar("CONTRACT_NEGOTIATION_ID") !== undefined && bru.getVar("CONTRACT_NEGOTIATION_ID") !== null){ + find_negotiation = res.getBody().find((el) => el["@id"] == bru.getVar("CONTRACT_NEGOTIATION_ID")) + } + + if(find_negotiation){ + const contractAgreementId = find_negotiation["contractAgreementId"]; + bru.setVar("CONTRACT_AGREEMENT_ID", contractAgreementId); + } + } + + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); + test("Contract Agreement ID is set", function(){ + expect(bru.getVar("CONTRACT_AGREEMENT_ID")).not.to.be.undefined + }) + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Get EDR DataAddress for TransferId.bru b/Requests/ControlPlane Management/Get EDR DataAddress for TransferId.bru new file mode 100644 index 000000000..7f744874e --- /dev/null +++ b/Requests/ControlPlane Management/Get EDR DataAddress for TransferId.bru @@ -0,0 +1,33 @@ +meta { + name: Get EDR DataAddress for TransferId + type: http + seq: 8 +} + +get { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/edrs/{{TRANSFER_PROCESS_ID}}/dataaddress + body: none + auth: inherit +} + +script:pre-request { + if(!(bru.getVar("TRANSFER_PROCESS_ID") !== undefined && bru.getVar("TRANSFER_PROCESS_ID") !== null)){ + throw new Error('Transfer Process ID is not yet available, please execute request "Get Transfer Processes" first!'); + } +} + +script:post-response { + // get the authorization token and save it as an environment variable + if(res.getStatus() < 300 && res.getStatus() >= 200){ + //using the first authorization token found + const authorization = res.getBody()["authorization"]; + bru.setVar("AUTHORIZATION", authorization); + } + + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Get cached EDRs.bru b/Requests/ControlPlane Management/Get cached EDRs.bru new file mode 100644 index 000000000..db0023d7a --- /dev/null +++ b/Requests/ControlPlane Management/Get cached EDRs.bru @@ -0,0 +1,50 @@ +meta { + name: Get cached EDRs + type: http + seq: 7 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/edrs/request + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +body:text { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +script:post-response { + // get the transfer process id of "asset-1" and save it as an environment variable if the response body is not empty + if(res.getStatus() < 300 && res.getStatus() >= 200 && res.getBody().length > 0){ + const transferProcessId = res.getBody()[0]["transferProcessId"]; + bru.setVar("TRANSFER_PROCESS_ID", transferProcessId); + } + + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); + test("Transfer process id is set", function(){ + expect(bru.getVar("TRANSFER_PROCESS_ID")).not.to.be.undefined + }) + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Get transfer processes.bru b/Requests/ControlPlane Management/Get transfer processes.bru new file mode 100644 index 000000000..90843374c --- /dev/null +++ b/Requests/ControlPlane Management/Get transfer processes.bru @@ -0,0 +1,32 @@ +meta { + name: Get transfer processes + type: http + seq: 6 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/transferprocesses/request + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +script:post-response { + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Initiate Transfer.bru b/Requests/ControlPlane Management/Initiate Transfer.bru new file mode 100644 index 000000000..32299fd1e --- /dev/null +++ b/Requests/ControlPlane Management/Initiate Transfer.bru @@ -0,0 +1,47 @@ +meta { + name: Initiate Transfer + type: http + seq: 5 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/transferprocesses + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "assetId": "asset-1", + "@type": "TransferRequest", + "counterPartyAddress": "{{PROVIDER_DSP}}", + "connectorId": "{{PROVIDER_ID}}", + "contractId": "{{CONTRACT_AGREEMENT_ID}}", + "dataDestination": { + "type": "HttpProxy" + }, + "protocol": "dataspace-protocol-http", + "transferType": "HttpData-PULL" + } +} + +script:pre-request { + if(!(bru.getVar("CONTRACT_AGREEMENT_ID") !== undefined && bru.getVar("CONTRACT_AGREEMENT_ID") !== null)){ + throw new Error('Contract Agreement ID is not yet available, please execute requests "Initiate Negotiation and Get Contract Negotiation" first!'); + } +} + +script:post-response { + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Initiate negotiation.bru b/Requests/ControlPlane Management/Initiate negotiation.bru new file mode 100644 index 000000000..69d104074 --- /dev/null +++ b/Requests/ControlPlane Management/Initiate negotiation.bru @@ -0,0 +1,62 @@ +meta { + name: Initiate negotiation + type: http + seq: 3 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/contractnegotiations + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "ContractRequest", + "counterPartyAddress": "{{PROVIDER_DSP}}", + "counterPartyId": "{{PROVIDER_ID}}", + "protocol": "dataspace-protocol-http:2025-1", + "policy": { + "@type": "Offer", + "@id": "{{POLICY_ID_ASSET_1}}", + "assigner": "{{PROVIDER_ID}}", + "permission": [], + "prohibition": [], + "obligation": { + "action": "use", + "constraint": { + "leftOperand": "ManufacturerCredential.part_types", + "operator": "eq", + "rightOperand": "non_critical" + } + }, + "target": "asset-1" + }, + "callbackAddresses": [] + } +} + +script:pre-request { + if(!(bru.getVar("POLICY_ID_ASSET_1") !== undefined && bru.getVar("POLICY_ID_ASSET_1") !== null)){ + throw new Error('Policy-ID of Asset-1 is not yet available, please execute request "Get Cached Catalog" first!'); + } +} + +script:post-response { + bru.setVar("CONTRACT_NEGOTIATION_ID", res.getBody()["@id"]) + + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + }); + test("Contract negotiation id is set", function(){ + expect(bru.getVar("CONTRACT_NEGOTIATION_ID")).not.to.be.undefined + }) +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/Request Catalog.bru b/Requests/ControlPlane Management/Request Catalog.bru new file mode 100644 index 000000000..b48c26160 --- /dev/null +++ b/Requests/ControlPlane Management/Request Catalog.bru @@ -0,0 +1,62 @@ +meta { + name: Request Catalog + type: http + seq: 2 +} + +post { + url: {{CONSUMER_CP}}/api/mgmt/v4beta/catalog/request + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CatalogRequest", + "counterPartyAddress": "{{PROVIDER_DSP}}", + "counterPartyId": "{{PROVIDER_ID}}", + "protocol": "dataspace-protocol-http:2025-1", + "querySpec": { + "offset": 0, + "limit": 50 + } + } +} + +body:text { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CatalogRequest", + "counterPartyAddress": "{{PROVIDER_DSP}}", + "counterPartyId": "{{PROVIDER_ID}}", + "protocol": "dataspace-protocol-http", + "querySpec": { + "offset": 0, + "limit": 50 + } + } +} + +script:post-response { + test("Status code is >=200 and <300", function () { + expect(res.getStatus() < 300 && res.getStatus() >= 200).to.be.true + + const dcat_datasets =res.getBody()["dataset"] + const asset_1 = dcat_datasets.find((ds) => ds["@id"] == "asset-1") + + bru.setVar("POLICY_ID_ASSET_1", asset_1["hasPolicy"][0]["@id"]); + console.log("asset id", bru.getVar("POLICY_ID_ASSET_1")) + }); + + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/ControlPlane Management/folder.bru b/Requests/ControlPlane Management/folder.bru new file mode 100644 index 000000000..e0d19a9c1 --- /dev/null +++ b/Requests/ControlPlane Management/folder.bru @@ -0,0 +1,8 @@ +meta { + name: ControlPlane Management + seq: 1 +} + +auth { + mode: inherit +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Add endpoint.bru b/Requests/IdentityHub/DID Document Mgmt API/Add endpoint.bru new file mode 100644 index 000000000..ea2380384 --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Add endpoint.bru @@ -0,0 +1,24 @@ +meta { + name: Add endpoint + type: http + seq: 4 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids/{{DID}}/endpoints + body: json + auth: inherit +} + +body:json { + { + "id": "some-other-id", + "type": "CredentialService", + "serviceEndpoint": "https://foobar.myconnector.com" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Delete DID.bru b/Requests/IdentityHub/DID Document Mgmt API/Delete DID.bru new file mode 100644 index 000000000..6b3bd5dc5 --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Delete DID.bru @@ -0,0 +1,22 @@ +meta { + name: Delete DID + type: http + seq: 6 +} + +delete { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids + body: json + auth: inherit +} + +body:json { + { + "did": "did:web:consumer-identityhub%3A7083:connector1" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Get All DID Documents.bru b/Requests/IdentityHub/DID Document Mgmt API/Get All DID Documents.bru new file mode 100644 index 000000000..b16cc61ef --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Get All DID Documents.bru @@ -0,0 +1,21 @@ +meta { + name: Get All DID Documents + type: http + seq: 2 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/dids + body: json + auth: inherit +} + +body:json { + { + + } +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Get DID Document state.bru b/Requests/IdentityHub/DID Document Mgmt API/Get DID Document state.bru new file mode 100644 index 000000000..4be975fdc --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Get DID Document state.bru @@ -0,0 +1,22 @@ +meta { + name: Get DID Document state + type: http + seq: 7 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids/state + body: json + auth: inherit +} + +body:json { + { + "did": "did:web:BPN0000001" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Publish DID.bru b/Requests/IdentityHub/DID Document Mgmt API/Publish DID.bru new file mode 100644 index 000000000..1b289a493 --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Publish DID.bru @@ -0,0 +1,22 @@ +meta { + name: Publish DID + type: http + seq: 3 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids/publish + body: json + auth: inherit +} + +body:json { + { + "did": "did:web:BPN0000001" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Query DIDs.bru b/Requests/IdentityHub/DID Document Mgmt API/Query DIDs.bru new file mode 100644 index 000000000..0a6932531 --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Query DIDs.bru @@ -0,0 +1,22 @@ +meta { + name: Query DIDs + type: http + seq: 1 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids/query + body: json + auth: inherit +} + +body:json { + { + + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/Un-Publish DID.bru b/Requests/IdentityHub/DID Document Mgmt API/Un-Publish DID.bru new file mode 100644 index 000000000..15f26c13b --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/Un-Publish DID.bru @@ -0,0 +1,22 @@ +meta { + name: Un-Publish DID + type: http + seq: 5 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/dids/unpublish + body: json + auth: inherit +} + +body:json { + { + "did": "did:web:consumer-identityhub%3A7083:connector1" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/DID Document Mgmt API/folder.bru b/Requests/IdentityHub/DID Document Mgmt API/folder.bru new file mode 100644 index 000000000..b5a8d3907 --- /dev/null +++ b/Requests/IdentityHub/DID Document Mgmt API/folder.bru @@ -0,0 +1,14 @@ +meta { + name: DID Document Mgmt API + seq: 3 +} + +auth { + mode: inherit +} + +script:post-response { + test("Status is OK", function() { + expect(res.getStatus()).to.be.oneOf([200, 204]) + }) +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/Add KeyPair.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/Add KeyPair.bru new file mode 100644 index 000000000..aacd5c000 --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/Add KeyPair.bru @@ -0,0 +1,33 @@ +meta { + name: Add KeyPair + type: http + seq: 3 +} + +put { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/keypairs + body: json + auth: inherit +} + +body:json { + { + "keyId": "key6", + "privateKeyAlias": "new-foobar-alias5", + "keyGeneratorParams": { + "algorithm": "EdDSA", + "curve": "Ed25519" + } + } +} + +script:post-response { + test("Status is OK or conflict", function() { + expect(res.getStatus()).to.be.oneOf([200, 204, 409]) + }) +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/Get KeyPair for Participant.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/Get KeyPair for Participant.bru new file mode 100644 index 000000000..588194fa6 --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/Get KeyPair for Participant.bru @@ -0,0 +1,16 @@ +meta { + name: Get KeyPair for Participant + type: http + seq: 1 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/keypairs + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/Get all KeyPairs.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/Get all KeyPairs.bru new file mode 100644 index 000000000..8e3162f44 --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/Get all KeyPairs.bru @@ -0,0 +1,15 @@ +meta { + name: Get all KeyPairs + type: http + seq: 2 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/keypairs + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/Revoke key.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/Revoke key.bru new file mode 100644 index 000000000..15849e23d --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/Revoke key.bru @@ -0,0 +1,16 @@ +meta { + name: Revoke key + type: http + seq: 5 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/keypairs/key6/revoke + body: json + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/Rotate key.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/Rotate key.bru new file mode 100644 index 000000000..445b739c8 --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/Rotate key.bru @@ -0,0 +1,27 @@ +meta { + name: Rotate key + type: http + seq: 4 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/keypairs/key1/rotate + body: json + auth: inherit +} + +body:json { + { + "keyId": "key2", + "privateKeyAlias": "new-foobar-alias", + "keyGeneratorParams": { + "algorithm": "EC", + "curve": "secp256r1" + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/KeyPair Resources Mgmt API/folder.bru b/Requests/IdentityHub/KeyPair Resources Mgmt API/folder.bru new file mode 100644 index 000000000..ba36ccf0e --- /dev/null +++ b/Requests/IdentityHub/KeyPair Resources Mgmt API/folder.bru @@ -0,0 +1,8 @@ +meta { + name: KeyPair Resources Mgmt API + seq: 2 +} + +auth { + mode: inherit +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Activate Participant.bru b/Requests/IdentityHub/Participant Context Mgmt API/Activate Participant.bru new file mode 100644 index 000000000..3b61ec23c --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Activate Participant.bru @@ -0,0 +1,38 @@ +meta { + name: Activate Participant + type: http + seq: 7 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/state?isActive=true + body: json + auth: inherit +} + +params:query { + isActive: true +} + +body:json { + { + "roles":[], + "serviceEndpoints":[], + "isActive": true, + "participantId": "foobar", + "did": "did:web:foobar", + "key":{ + "keyId": "key1", + "privateKeyAlias": "foobar-alias", + "keyGeneratorParams":{ + "algorithm": "EC", + "curve": "secp256r1" + } + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Create Participant (existing key).bru b/Requests/IdentityHub/Participant Context Mgmt API/Create Participant (existing key).bru new file mode 100644 index 000000000..b94cb7f9f --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Create Participant (existing key).bru @@ -0,0 +1,30 @@ +meta { + name: Create Participant (existing key) + type: http + seq: 4 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/ + body: json + auth: inherit +} + +body:json { + { + "roles":[], + "serviceEndpoints":[], + "active": true, + "participantId": "{{NEW_PARTICIPANT_ID}}", + "did": "{{NEW_PARTICIPANT_ID}}", + "key":{ + "keyId": "key-1", + "privateKeyAlias": "{{NEW_PARTICIPANT_ID}}-alias", + "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1l0Lof0a1yBc8KXhesAnoBvxZw5r\noYnkAXuqCYfNK3ex+hMWFuiXGUxHlzShAehR6wvwzV23bbC0tcFcVgW//A==\n-----END PUBLIC KEY-----" + } + } +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Create Participant.bru b/Requests/IdentityHub/Participant Context Mgmt API/Create Participant.bru new file mode 100644 index 000000000..456a7f2de --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Create Participant.bru @@ -0,0 +1,33 @@ +meta { + name: Create Participant + type: http + seq: 3 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/ + body: json + auth: inherit +} + +body:json { + { + "roles":[], + "serviceEndpoints":[], + "active": true, + "participantId": "{{NEW_PARTICIPANT_ID}}", + "did": "{{NEW_PARTICIPANT_ID}}", + "key":{ + "keyId": "key-1", + "privateKeyAlias": "{{NEW_PARTICIPANT_ID}}-alias", + "keyGeneratorParams":{ + "algorithm": "EdDSA", + "curve": "Ed25519" + } + } + } +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Deactivate Participant.bru b/Requests/IdentityHub/Participant Context Mgmt API/Deactivate Participant.bru new file mode 100644 index 000000000..69b1d55c4 --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Deactivate Participant.bru @@ -0,0 +1,37 @@ +meta { + name: Deactivate Participant + type: http + seq: 8 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}}/state?isActive=false + body: json + auth: inherit +} + +params:query { + isActive: false +} + +body:json { + { + "roles":[], + "serviceEndpoints":[], + "isActive": true, + "participantId": "foobar", + "did": "did:web:foobar", + "key":{ + "keyId": "key1", + "privateKeyAlias": "foobar-alias", + "keyGeneratorParams":{ + "algorithm": "EC", + "curve": "secp256r1" + } + } + } +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Delete Participant.bru b/Requests/IdentityHub/Participant Context Mgmt API/Delete Participant.bru new file mode 100644 index 000000000..ef2c25afa --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Delete Participant.bru @@ -0,0 +1,33 @@ +meta { + name: Delete Participant + type: http + seq: 9 +} + +delete { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_ID}} + body: json + auth: inherit +} + +body:json { + { + "roles":[], + "serviceEndpoints":[], + "isActive": true, + "participantId": "foobar", + "did": "did:web:foobar", + "key":{ + "keyId": "key1", + "privateKeyAlias": "foobar-alias", + "keyGeneratorParams":{ + "algorithm": "EC", + "curve": "secp256r1" + } + } + } +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Get Participant By ID.bru b/Requests/IdentityHub/Participant Context Mgmt API/Get Participant By ID.bru new file mode 100644 index 000000000..f5a336b56 --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Get Participant By ID.bru @@ -0,0 +1,16 @@ +meta { + name: Get Participant By ID + type: http + seq: 1 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}} + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Get all participants.bru b/Requests/IdentityHub/Participant Context Mgmt API/Get all participants.bru new file mode 100644 index 000000000..af017927d --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Get all participants.bru @@ -0,0 +1,15 @@ +meta { + name: Get all participants + type: http + seq: 2 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Regenerate Token.bru b/Requests/IdentityHub/Participant Context Mgmt API/Regenerate Token.bru new file mode 100644 index 000000000..1ff52409c --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Regenerate Token.bru @@ -0,0 +1,21 @@ +meta { + name: Regenerate Token + type: http + seq: 6 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/token + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Accept: application/json +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/Update Roles.bru b/Requests/IdentityHub/Participant Context Mgmt API/Update Roles.bru new file mode 100644 index 000000000..20a2f6c7a --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/Update Roles.bru @@ -0,0 +1,22 @@ +meta { + name: Update Roles + type: http + seq: 5 +} + +put { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/roles + body: json + auth: inherit +} + +body:json { + [ + "role1", "role2", "admin" + ] +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/Participant Context Mgmt API/folder.bru b/Requests/IdentityHub/Participant Context Mgmt API/folder.bru new file mode 100644 index 000000000..731bda058 --- /dev/null +++ b/Requests/IdentityHub/Participant Context Mgmt API/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Participant Context Mgmt API + seq: 1 +} + +auth { + mode: inherit +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Add VerifiableCredential.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Add VerifiableCredential.bru new file mode 100644 index 000000000..f36cb2cee --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Add VerifiableCredential.bru @@ -0,0 +1,61 @@ +meta { + name: Add VerifiableCredential + type: http + seq: 5 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/credentials + body: json + auth: inherit +} + +body:json { + { + "id": "verifiable-credential-1", + "participantContextId": "{{PARTICIPANT_CONTEXT_ID}}", + "issuancePolicy": null, + "reissuancePolicy": null, + "verifiableCredentialContainer": { + "format": "VC1_0_JWT", + "rawVc": "eyJraWQiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIubXZkLWlzc3Vlci5zdmMuY2x1c3Rlci5sb2NhbCNrZXktMSIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJpc3MiOiJkaWQ6d2ViOmRhdGFzcGFjZS1pc3N1ZXIubXZkLWlzc3Vlci5zdmMuY2x1c3Rlci5sb2NhbCIsImF1ZCI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIubXZkLWNvbnN1bWVyLXNlY3VyaXR5LnN2Yy5jbHVzdGVyLmxvY2FsJTNBNzA4Mzpjb25zdW1lciIsInN1YiI6ImRpZDp3ZWI6Y29uc3VtZXItaWRlbnRpdHlodWIubXZkLWNvbnN1bWVyLXNlY3VyaXR5LnN2Yy5jbHVzdGVyLmxvY2FsJTNBNzA4Mzpjb25zdW1lciIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93M2lkLm9yZy9zZWN1cml0eS9zdWl0ZXMvandzLTIwMjAvdjEiLCJodHRwczovL3d3dy53My5vcmcvbnMvZGlkL3YxIix7Im12ZC1jcmVkZW50aWFscyI6Imh0dHBzOi8vdzNpZC5vcmcvbXZkL2NyZWRlbnRpYWxzLyIsImNvbnRyYWN0VmVyc2lvbiI6Im12ZC1jcmVkZW50aWFsczpjb250cmFjdFZlcnNpb24iLCJsZXZlbCI6Im12ZC1jcmVkZW50aWFsczpsZXZlbCJ9XSwiaWQiOiJodHRwOi8vb3JnLnlvdXJkYXRhc3BhY2UuY29tL2NyZWRlbnRpYWxzLzIzNDciLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRGF0YVByb2Nlc3NvckNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOndlYjpkYXRhc3BhY2UtaXNzdWVyLm12ZC1pc3N1ZXIuc3ZjLmNsdXN0ZXIubG9jYWwiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA4LTE4VDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjpjb25zdW1lci1pZGVudGl0eWh1Yi5tdmQtY29uc3VtZXItc2VjdXJpdHkuc3ZjLmNsdXN0ZXIubG9jYWwlM0E3MDgzOmNvbnN1bWVyIiwiY29udHJhY3RWZXJzaW9uIjoiMS4wLjAiLCJsZXZlbCI6InByb2Nlc3NpbmcifX0sImlhdCI6MTc1Mjc1MzA2NX0.aBs8_vc-LPIkmci1-fWU_TEAm-Nze8SQEiho_sSIdzS220RooPaMzJadiPetKBMopua_qddxYjefWRmUtGEvAw", + "credential": { + "credentialSubject": [ + { + "id": "did:web:consumer-identityhub.mvd-consumer-security.svc.cluster.local%3A7083:consumer", + "contractVersion": "1.0.0", + "level": "processing" + } + ], + "id": "http://org.yourdataspace.com/credentials/1235", + "type": [ + "VerifiableCredential", + "DataProcessorCredential" + ], + "issuer": { + "id": "did:web:dataspace-issuer.mvd-issuer.svc.cluster.local", + "additionalProperties": {} + }, + "issuanceDate": 1702339200.000000000, + "expirationDate": null, + "credentialStatus": null, + "description": null, + "name": null + } + } + } +} + +script:post-response { + if(res.getStatus() < 300 && res.getStatus() >= 200){ + if(JSON.stringify(res.getBody()).length > 0){ + const requestId = JSON.stringify(res.getBody()) + bru.setEnvVar("REQUEST_ID", requestId); + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Get All Credentials.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get All Credentials.bru new file mode 100644 index 000000000..1160db5ce --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get All Credentials.bru @@ -0,0 +1,21 @@ +meta { + name: Get All Credentials + type: http + seq: 2 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/credentials + body: none + auth: inherit +} + +script:post-response { + test("Status is OK", function() { + expect(res.getStatus()).to.be.oneOf([200, 204]) + }) +} + +settings { + encodeUrl: true +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential By ID.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential By ID.bru new file mode 100644 index 000000000..994a19deb --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential By ID.bru @@ -0,0 +1,22 @@ +meta { + name: Get Credential By ID + type: http + seq: 1 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/credentials/CREDENTIAL-ID + body: none + auth: inherit +} + +script:post-response { + test("Status is OK", function() { + expect(res.getStatus()).to.be.oneOf([200, 204]) + }) +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential Reqeusts.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential Reqeusts.bru new file mode 100644 index 000000000..0a6714c7c --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Get Credential Reqeusts.bru @@ -0,0 +1,16 @@ +meta { + name: Get Credential Reqeusts + type: http + seq: 6 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/credentials/request/{{REQUEST_ID}} + body: json + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Make Credential Request.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Make Credential Request.bru new file mode 100644 index 000000000..4b9427e2a --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Make Credential Request.bru @@ -0,0 +1,42 @@ +meta { + name: Make Credential Request + type: http + seq: 4 +} + +post { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/credentials/request + body: json + auth: inherit +} + +body:json { + { + "issuerDid": "{{ISSUER_DID}}", + "holderPid": "{{$guid}}", + "credentials": [{ + "format": "VC1_0_JWT", + "type": "MembershipCredential", + "id": "membership-credential-def" + }, + { + "format": "VC1_0_JWT", + "type": "ManufacturerCredential", + "id": "manufacturer-credential-def" + }] + } +} + +script:post-response { + if(res.getStatus() < 300 && res.getStatus() >= 200){ + const loc =  res.getHeader("location") + const requestId = loc.split("/").filter(Boolean).pop() + bru.setVar("REQUEST_ID", requestId); + + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/Query Credentials by Type.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/Query Credentials by Type.bru new file mode 100644 index 000000000..31a1602d7 --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/Query Credentials by Type.bru @@ -0,0 +1,26 @@ +meta { + name: Query Credentials by Type + type: http + seq: 3 +} + +get { + url: {{CS_URL}}/api/identity/v1alpha/participants/{{PARTICIPANT_CONTEXT_ID}}/credentials?type=ManufacturerCredential + body: none + auth: inherit +} + +params:query { + type: ManufacturerCredential +} + +script:post-response { + test("Status is OK", function() { + expect(res.getStatus()).to.be.oneOf([200, 204]) + }) +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IdentityHub/VerifiableCredential Mgmt API/folder.bru b/Requests/IdentityHub/VerifiableCredential Mgmt API/folder.bru new file mode 100644 index 000000000..808a702de --- /dev/null +++ b/Requests/IdentityHub/VerifiableCredential Mgmt API/folder.bru @@ -0,0 +1,8 @@ +meta { + name: VerifiableCredential Mgmt API + seq: 4 +} + +auth { + mode: inherit +} diff --git a/Requests/IdentityHub/folder.bru b/Requests/IdentityHub/folder.bru new file mode 100644 index 000000000..e663f17f9 --- /dev/null +++ b/Requests/IdentityHub/folder.bru @@ -0,0 +1,29 @@ +meta { + name: IdentityHub +} + +auth { + mode: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: http://keycloak.localhost/realms/mvd/protocol/openid-connect/token + refresh_token_url: + client_id: admin + client_secret: edc-v-admin-secret + scope: + credentials_placement: body + credentials_id: credentials + token_source: access_token + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} + +script:post-response { + test("Status is OK or conflict", function() { + expect(res.getStatus()).to.be.oneOf([200, 201, 204, 409]) + }) +} diff --git a/Requests/IssuerService/Admin API/Attestations/create Attestation.bru b/Requests/IssuerService/Admin API/Attestations/create Attestation.bru new file mode 100644 index 000000000..abe67e8c5 --- /dev/null +++ b/Requests/IssuerService/Admin API/Attestations/create Attestation.bru @@ -0,0 +1,292 @@ +meta { + name: create Attestation + type: http + seq: 1 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations + body: json + auth: inherit +} + +body:json { + { + "attestationType": "database", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } +} + +settings { + encodeUrl: true +} + +docs { + Creates an attestation definition in the runtime. +} + +example { + name: The attestation definition was added successfully. + + request: { + url: {{baseUrl}}/v1alpha/attestations + method: POST + mode: json + headers: { + Content-Type: application/json + } + + body:json: { + { + "attestationType": "", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } + } + } + + response: { + status: { + code: 201 + text: Created + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/attestations + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestationType": "", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/attestations + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestationType": "", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/attestations + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestationType": "", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: Can't add the participant, because a object with the same ID already exists + + request: { + url: {{baseUrl}}/v1alpha/attestations + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestationType": "", + "configuration": { + "commodof4": {}, + "laborisb27": {} + }, + "id": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 409 + text: Conflict + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Attestations/folder.bru b/Requests/IssuerService/Admin API/Attestations/folder.bru new file mode 100644 index 000000000..3960f8324 --- /dev/null +++ b/Requests/IssuerService/Admin API/Attestations/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Attestations + seq: 1 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/Admin API/Attestations/query Attestations.bru b/Requests/IssuerService/Admin API/Attestations/query Attestations.bru new file mode 100644 index 000000000..44a20119c --- /dev/null +++ b/Requests/IssuerService/Admin API/Attestations/query Attestations.bru @@ -0,0 +1,227 @@ +meta { + name: query Attestations + type: http + seq: 2 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/attestations/query + body: json + auth: inherit +} + +body:json { + { + } +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + Query attestation definitions +} + +example { + name: A list of attestation metadata. + + request: { + url: {{baseUrl}}/v1alpha/attestations/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + [ + { + "attestationType": "", + "configuration": { + "aliquip2": {}, + "pariatur_b3": {}, + "essebab": {} + }, + "id": "" + }, + { + "attestationType": "", + "configuration": { + "sint_9f6": {} + }, + "id": "" + } + ] + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/attestations/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/attestations/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/create Credential Definition.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/create Credential Definition.bru new file mode 100644 index 000000000..7df76a733 --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/create Credential Definition.bru @@ -0,0 +1,386 @@ +meta { + name: create Credential Definition + type: http + seq: 1 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions + body: json + auth: inherit +} + +body:json { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "", + "formats": ["VC1_0_JWT"] + } +} + +settings { + encodeUrl: true +} + +docs { + Adds a new credential definition. +} + +example { + name: The credential definition was created successfully. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: POST + mode: json + headers: { + Content-Type: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + status: { + code: 201 + text: Created + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: Can't create the credential definition, because a object with the same ID already exists + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 409 + text: Conflict + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/delete Credential Definition By Id.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/delete Credential Definition By Id.bru new file mode 100644 index 000000000..8ea63d4cb --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/delete Credential Definition By Id.bru @@ -0,0 +1,243 @@ +meta { + name: delete Credential Definition By Id + type: http + seq: 5 +} + +delete { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/:credentialDefinitionId + body: none + auth: inherit +} + +params:path { + credentialDefinitionId: +} + +headers { + Accept: application/json +} + +settings { + encodeUrl: true +} + +docs { + Deletes a credential definition by its ID. +} + +example { + name: The credential definition was deleted. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: DELETE + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: DELETE + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: DELETE + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential definition was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: DELETE + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/folder.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/folder.bru new file mode 100644 index 000000000..5c9d36d22 --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/folder.bru @@ -0,0 +1,8 @@ +meta { + name: CredentialDefinitions + seq: 2 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/get by ID.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/get by ID.bru new file mode 100644 index 000000000..70ba6bfec --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/get by ID.bru @@ -0,0 +1,243 @@ +meta { + name: get by ID + type: http + seq: 3 +} + +get { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/:credentialDefinitionId + body: none + auth: inherit +} + +params:path { + credentialDefinitionId: +} + +headers { + Accept: application/json +} + +settings { + encodeUrl: true +} + +docs { + Gets a credential definition by its ID. +} + +example { + name: The credential definition was found. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: GET + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: GET + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: GET + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential definition was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/:credentialDefinitionId + method: GET + mode: none + params:path: { + credentialDefinitionId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/query Credential Definitions.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/query Credential Definitions.bru new file mode 100644 index 000000000..7642f8cd5 --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/query Credential Definitions.bru @@ -0,0 +1,292 @@ +meta { + name: query Credential Definitions + type: http + seq: 2 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions/query + body: json + auth: inherit +} + +body:json { + { + } +} + +settings { + encodeUrl: true +} + +docs { + Gets all credential definitions for a certain query. +} + +example { + name: A list of credentials definitions. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + [ + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_1_1", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "in_85a": {}, + "dolor310": {}, + "incididunt_b": {} + }, + "type": "" + }, + { + "configuration": { + "eud_": {}, + "dolor777": {}, + "Utb": {} + }, + "type": "" + } + ], + "validity": "" + }, + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "laborum7": {}, + "voluptate726": {} + }, + "type": "" + }, + { + "configuration": { + "amet_27c": {}, + "voluptate8": {} + }, + "type": "" + } + ], + "validity": "" + } + ] + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/CredentialDefinitions/update Credential Definition.bru b/Requests/IssuerService/Admin API/CredentialDefinitions/update Credential Definition.bru new file mode 100644 index 000000000..d190c5715 --- /dev/null +++ b/Requests/IssuerService/Admin API/CredentialDefinitions/update Credential Definition.bru @@ -0,0 +1,391 @@ +meta { + name: update Credential Definition + type: http + seq: 4 +} + +put { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentialdefinitions + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } +} + +settings { + encodeUrl: true +} + +docs { + Updates credential definition. +} + +example { + name: The credential definition was updated successfully. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: PUT + mode: json + headers: { + Content-Type: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + status: { + code: 200 + text: OK + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: Can't update the credential definition because it was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentialdefinitions + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "attestations": [ + "", + "" + ], + "credentialType": "", + "dataModel": "V_2_0", + "id": "", + "jsonSchema": "", + "jsonSchemaUrl": "", + "mappings": [ + { + "input": "", + "output": "", + "required": "" + }, + { + "input": "", + "output": "", + "required": "" + } + ], + "rules": [ + { + "configuration": { + "addca": {} + }, + "type": "" + }, + { + "configuration": { + "incididunt_a1f": {} + }, + "type": "" + } + ], + "validity": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/check Credential Status.bru b/Requests/IssuerService/Admin API/Credentials/check Credential Status.bru new file mode 100644 index 000000000..d1679bb49 --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/check Credential Status.bru @@ -0,0 +1,194 @@ +meta { + name: check Credential Status + type: http + seq: 4 +} + +get { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/status + body: none + auth: inherit +} + +params:path { + credentialId: +} + +settings { + encodeUrl: true +} + +docs { + Checks the revocation status of a credential with the given ID for the given participant. +} + +example { + name: The credential status. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/status + method: GET + mode: none + params:path: { + credentialId: + } + } + + response: { + status: { + code: 200 + text: OK + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/status + method: GET + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/status + method: GET + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential or the participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/status + method: GET + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/folder.bru b/Requests/IssuerService/Admin API/Credentials/folder.bru new file mode 100644 index 000000000..88d57fccd --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Credentials + seq: 3 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/Admin API/Credentials/get Credentials.bru b/Requests/IssuerService/Admin API/Credentials/get Credentials.bru new file mode 100644 index 000000000..1a2b41ed1 --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/get Credentials.bru @@ -0,0 +1,299 @@ +meta { + name: get Credentials + type: http + seq: 6 +} + +get { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/{{PARTICIPANT_ID}} + body: none + auth: inherit +} + +settings { + encodeUrl: true +} + +docs { + Gets all credentials for a certain participant. +} + +example { + name: A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + [ + { + "credential": { + "credentialSchema": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialStatus": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialSubject": [ + { + "id": "" + }, + { + "id": "" + } + ], + "dataModelVersion": "V_2_0", + "description": "", + "expirationDate": "", + "id": "", + "issuanceDate": "", + "issuer": { + "additionalProperties": { + "ex_991": {} + }, + "id": "" + }, + "name": "", + "type": [ + "", + "" + ] + }, + "format": "JSON_LD" + }, + { + "credential": { + "credentialSchema": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialStatus": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialSubject": [ + { + "id": "" + }, + { + "id": "" + } + ], + "dataModelVersion": "V_1_1", + "description": "", + "expirationDate": "", + "id": "", + "issuanceDate": "", + "issuer": { + "additionalProperties": { + "enima7": {}, + "ad_a5": {}, + "ipsum_65": {} + }, + "id": "" + }, + "name": "", + "type": [ + "", + "" + ] + }, + "format": "JWT" + } + ] + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/query Credentials.bru b/Requests/IssuerService/Admin API/Credentials/query Credentials.bru new file mode 100644 index 000000000..414a03ff2 --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/query Credentials.bru @@ -0,0 +1,310 @@ +meta { + name: query Credentials + type: http + seq: 1 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/query + body: json + auth: inherit +} + +body:json { + { + } +} + +settings { + encodeUrl: true +} + +docs { + Query credentials, possibly across multiple participants. +} + +example { + name: A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials. + + request: { + url: {{baseUrl}}/v1alpha/credentials/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + [ + { + "credential": { + "credentialSchema": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialStatus": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialSubject": [ + { + "id": "" + }, + { + "id": "" + } + ], + "dataModelVersion": "V_2_0", + "description": "", + "expirationDate": "", + "id": "", + "issuanceDate": "", + "issuer": { + "additionalProperties": { + "ex_991": {} + }, + "id": "" + }, + "name": "", + "type": [ + "", + "" + ] + }, + "format": "JSON_LD" + }, + { + "credential": { + "credentialSchema": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialStatus": [ + { + "id": "", + "type": "" + }, + { + "id": "", + "type": "" + } + ], + "credentialSubject": [ + { + "id": "" + }, + { + "id": "" + } + ], + "dataModelVersion": "V_1_1", + "description": "", + "expirationDate": "", + "id": "", + "issuanceDate": "", + "issuer": { + "additionalProperties": { + "enima7": {}, + "ad_a5": {}, + "ipsum_65": {} + }, + "id": "" + }, + "name": "", + "type": [ + "", + "" + ] + }, + "format": "JWT" + } + ] + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/resume Credential.bru b/Requests/IssuerService/Admin API/Credentials/resume Credential.bru new file mode 100644 index 000000000..a90385b0c --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/resume Credential.bru @@ -0,0 +1,198 @@ +meta { + name: resume Credential + type: http + seq: 2 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/resume + body: none + auth: inherit +} + +params:path { + credentialId: +} + +headers { + Accept: application/json +} + +settings { + encodeUrl: true +} + +docs { + Resumes a credential with the given ID for the given participant. Resumed credentials will be removed from the Revocation List. +} + +example { + name: The credential was resumed successfully. Check the Revocation List credential to confirm. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/resume + method: POST + mode: none + params:path: { + credentialId: + } + } + + response: { + status: { + code: 204 + text: No Content + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/resume + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/resume + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential or the participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/resume + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/revoke Credential.bru b/Requests/IssuerService/Admin API/Credentials/revoke Credential.bru new file mode 100644 index 000000000..52ec27e37 --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/revoke Credential.bru @@ -0,0 +1,194 @@ +meta { + name: revoke Credential + type: http + seq: 3 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/revoke + body: none + auth: inherit +} + +params:path { + credentialId: +} + +settings { + encodeUrl: true +} + +docs { + Revokes a credential with the given ID for the given participant. Revoked credentials will be added to the Revocation List +} + +example { + name: The credential was revoked successfully. Check the Revocation List credential to confirm. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/revoke + method: POST + mode: none + params:path: { + credentialId: + } + } + + response: { + status: { + code: 204 + text: No Content + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/revoke + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/revoke + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential or the participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/revoke + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Credentials/suspend Credential.bru b/Requests/IssuerService/Admin API/Credentials/suspend Credential.bru new file mode 100644 index 000000000..0029d58eb --- /dev/null +++ b/Requests/IssuerService/Admin API/Credentials/suspend Credential.bru @@ -0,0 +1,194 @@ +meta { + name: suspend Credential + type: http + seq: 5 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/credentials/:credentialId/suspend + body: none + auth: inherit +} + +params:path { + credentialId: +} + +settings { + encodeUrl: true +} + +docs { + Suspends a credential with the given ID for the given participant. Suspended credentials will be added to the Revocation List. Suspension is reversible. +} + +example { + name: The credential was suspended successfully. Check the Revocation List credential to confirm. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/suspend + method: POST + mode: none + params:path: { + credentialId: + } + } + + response: { + status: { + code: 204 + text: No Content + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/suspend + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/suspend + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The credential or the participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/credentials/:credentialId/suspend + method: POST + mode: none + params:path: { + credentialId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/IssuanceProcesses/Query issuance processes.bru b/Requests/IssuerService/Admin API/IssuanceProcesses/Query issuance processes.bru new file mode 100644 index 000000000..89f48ff62 --- /dev/null +++ b/Requests/IssuerService/Admin API/IssuanceProcesses/Query issuance processes.bru @@ -0,0 +1,22 @@ +meta { + name: Query issuance processes + type: http + seq: 1 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/issuanceprocesses/query + body: json + auth: inherit +} + +body:json { + { + + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Requests/IssuerService/Admin API/IssuanceProcesses/folder.bru b/Requests/IssuerService/Admin API/IssuanceProcesses/folder.bru new file mode 100644 index 000000000..d6c149be7 --- /dev/null +++ b/Requests/IssuerService/Admin API/IssuanceProcesses/folder.bru @@ -0,0 +1,8 @@ +meta { + name: IssuanceProcesses + seq: 5 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/Admin API/Participants/Create Participant.bru b/Requests/IssuerService/Admin API/Participants/Create Participant.bru new file mode 100644 index 000000000..d6ff857cf --- /dev/null +++ b/Requests/IssuerService/Admin API/Participants/Create Participant.bru @@ -0,0 +1,221 @@ +meta { + name: Create Participant + type: http + seq: 2 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders + body: json + auth: inherit +} + +body:json { + { + "did": "did:web:test", + "participantId": "did:web:test", + "name": "Test Participant" + } +} + +settings { + encodeUrl: true +} + +docs { + Adds a new participant. +} + +example { + name: The participant was added successfully. + + request: { + url: {{baseUrl}}/v1alpha/participants + method: POST + mode: json + headers: { + Content-Type: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + status: { + code: 201 + text: Created + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/participants + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/participants + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: Can't add the participant, because a object with the same ID already exists + + request: { + url: {{baseUrl}}/v1alpha/participants + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 409 + text: Conflict + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Participants/folder.bru b/Requests/IssuerService/Admin API/Participants/folder.bru new file mode 100644 index 000000000..cee6f219e --- /dev/null +++ b/Requests/IssuerService/Admin API/Participants/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Participants + seq: 4 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/Admin API/Participants/get Participant By Id.bru b/Requests/IssuerService/Admin API/Participants/get Participant By Id.bru new file mode 100644 index 000000000..bc2ebdc5d --- /dev/null +++ b/Requests/IssuerService/Admin API/Participants/get Participant By Id.bru @@ -0,0 +1,214 @@ +meta { + name: get Participant By Id + type: http + seq: 4 +} + +get { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders/:participantId + body: none + auth: inherit +} + +params:path { + participantId: +} + +headers { + Accept: application/json +} + +settings { + encodeUrl: true +} + +docs { + Gets metadata for a certain participant. +} + +example { + name: A list of verifiable credential metadata. Note that these are not actual VerifiableCredentials. + + request: { + url: {{baseUrl}}/v1alpha/participants/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + { + "attestations": [ + "", + "" + ], + "did": "", + "participantId": "", + "participantName": "" + } + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/participants/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/participants/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/participants/:participantId + method: GET + mode: none + params:path: { + participantId: + } + + headers: { + Accept: application/json + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Participants/query Participants.bru b/Requests/IssuerService/Admin API/Participants/query Participants.bru new file mode 100644 index 000000000..eccaa1939 --- /dev/null +++ b/Requests/IssuerService/Admin API/Participants/query Participants.bru @@ -0,0 +1,286 @@ +meta { + name: query Participants + type: http + seq: 3 +} + +post { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders/query + body: json + auth: inherit +} + +body:json { + { + } +} + +settings { + encodeUrl: true +} + +docs { + Gets all participants for a certain query. +} + +example { + name: A list of participant metadata. + + request: { + url: {{baseUrl}}/v1alpha/participants/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 200 + text: OK + } + + body: { + type: json + content: ''' + [ + { + "did": "", + "participantId": "", + "name": "" + }, + { + "did": "", + "participantId": "", + "name": "" + } + ] + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/participants/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/participants/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The participant was not found. + + request: { + url: {{baseUrl}}/v1alpha/participants/query + method: POST + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "filterExpression": [ + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + }, + { + "operandLeft": {}, + "operandRight": {}, + "operator": "" + } + ], + "limit": "", + "offset": "", + "sortField": "", + "sortOrder": "DESC" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/Participants/update Participant.bru b/Requests/IssuerService/Admin API/Participants/update Participant.bru new file mode 100644 index 000000000..ce72c1e4f --- /dev/null +++ b/Requests/IssuerService/Admin API/Participants/update Participant.bru @@ -0,0 +1,221 @@ +meta { + name: update Participant + type: http + seq: 1 +} + +put { + url: {{ISSUER_ADMIN_URL}}/api/admin/v1alpha/participants/{{ISSUER_CONTEXT_ID}}/holders + body: json + auth: inherit +} + +body:json { + { + "did": "", + "participantId": "", + "name": "" + } +} + +settings { + encodeUrl: true +} + +docs { + Updates participant data. +} + +example { + name: The participant was updated successfully. + + request: { + url: {{baseUrl}}/v1alpha/participants + method: PUT + mode: json + headers: { + Content-Type: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + status: { + code: 200 + text: OK + } + + body: { + type: text + content: ''' + + ''' + } + } +} + +example { + name: Request body was malformed, or the request could not be processed + + request: { + url: {{baseUrl}}/v1alpha/participants + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 400 + text: Bad Request + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: The request could not be completed, because either the authentication was missing or was not valid. + + request: { + url: {{baseUrl}}/v1alpha/participants + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 401 + text: Unauthorized + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} + +example { + name: Can't update the participant because it was not found. + + request: { + url: {{baseUrl}}/v1alpha/participants + method: PUT + mode: json + headers: { + Content-Type: application/json + Accept: application/json + } + + body:json: { + { + "did": "", + "participantId": "", + "name": "" + } + } + } + + response: { + headers: { + Content-Type: application/json + } + + status: { + code: 404 + text: Not Found + } + + body: { + type: json + content: ''' + [ + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + }, + { + "invalidValue": {}, + "message": "", + "path": "", + "type": "" + } + ] + ''' + } + } +} diff --git a/Requests/IssuerService/Admin API/folder.bru b/Requests/IssuerService/Admin API/folder.bru new file mode 100644 index 000000000..b4798be92 --- /dev/null +++ b/Requests/IssuerService/Admin API/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Admin API + seq: 1 +} + +auth { + mode: inherit +} diff --git a/Requests/IssuerService/folder.bru b/Requests/IssuerService/folder.bru new file mode 100644 index 000000000..bcee36bea --- /dev/null +++ b/Requests/IssuerService/folder.bru @@ -0,0 +1,23 @@ +meta { + name: IssuerService +} + +auth { + mode: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: http://keycloak.localhost/realms/mvd/protocol/openid-connect/token + refresh_token_url: + client_id: admin + client_secret: edc-v-admin-secret + scope: + credentials_placement: body + credentials_id: credentials + token_source: access_token + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: true +} diff --git a/Requests/bruno.json b/Requests/bruno.json new file mode 100644 index 000000000..a5501cd90 --- /dev/null +++ b/Requests/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "MVD", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/Requests/collection.bru b/Requests/collection.bru new file mode 100644 index 000000000..3b58a45bc --- /dev/null +++ b/Requests/collection.bru @@ -0,0 +1,24 @@ +meta { + name: MVD +} + +auth { + mode: apikey +} + +auth:apikey { + key: X-Api-Key + value: password + placement: header +} +vars:pre-request { + CONSUMER_CP: http://cp.consumer.localhost + PROVIDER_DSP: http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1 + PROVIDER_ID: did:web:identityhub.provider.svc.cluster.local%3A7083:provider + CS_URL: http://ih.consumer.localhost/cs + ISSUER_ADMIN_URL: http://issuer.localhost/admin + ISSUER_DID: did:web:localhost%3A10100 + PROVIDER_PUBLIC_URL: http://dp.provider.localhost/public + PARTICIPANT_CONTEXT_ID: consumer-participant + ISSUER_CONTEXT_ID: issuer +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8fbacab6..800ba1b51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,8 @@ edc-api-observability = { module = "org.eclipse.edc:api-observability", version. edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dataplane-v2 = { module = "org.eclipse.edc:data-plane-public-api-v2", version.ref = "edc" } edc-core-cel = { module = "org.eclipse.edc:cel-core", version.ref = "edc" } +edc-decentralized-claims-cel = { module = "org.eclipse.edc:decentralized-claims-cel", version.ref = "edc" } + edc-dcp-core = { module = "org.eclipse.edc:decentralized-claims-core", version.ref = "edc" } edc-vault-hashicorp = { module = "org.eclipse.edc:vault-hashicorp", version.ref = "edc" } edc-spi-identity-trust = { module = "org.eclipse.edc:decentralized-claims-spi", version.ref = "edc" } diff --git a/k8s/consumer/application/controlplane-seed.yaml b/k8s/consumer/application/controlplane-seed.yaml index 335096559..449f559b5 100644 --- a/k8s/consumer/application/controlplane-seed.yaml +++ b/k8s/consumer/application/controlplane-seed.yaml @@ -99,7 +99,7 @@ spec: "https://w3id.org/edc/connector/management/v2" ], "@type": "CelExpression", - "@id": "membership-cel", + "@id": "manufacturer-cel", "leftOperand": "ManufacturerCredential", "description": "Expression for evaluating manufacturer credential", "scopes": [ @@ -107,7 +107,7 @@ spec: "contract.negotiation", "transfer.process" ], - "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''ManufacturerCredential'\'')).size() > 0" + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t == '\''ManufacturerCredential'\'')).exists(c, c.credentialSubject.exists(cs, cs.part_types == this.rightOperand))" }' echo "✓ ManufacturerCredential CEL Expression done" diff --git a/k8s/issuer/application/issuerservice.yaml b/k8s/issuer/application/issuerservice.yaml index 6a0a57905..677115059 100644 --- a/k8s/issuer/application/issuerservice.yaml +++ b/k8s/issuer/application/issuerservice.yaml @@ -106,7 +106,7 @@ metadata: namespace: issuer spec: parentRefs: - - name: edcv-gateway + - name: issuer-gateway kind: Gateway sectionName: http hostnames: diff --git a/k8s/provider/application/controlplane-config.yaml b/k8s/provider/application/controlplane-config.yaml index 29d43a6fc..cb756c65f 100644 --- a/k8s/provider/application/controlplane-config.yaml +++ b/k8s/provider/application/controlplane-config.yaml @@ -25,8 +25,6 @@ data: edc.participant.id: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" edc.iam.did.web.use.https: "false" edc.iam.credential.revocation.mimetype: "application/json" - # todo: should this be true? - edc.policy.validation.enabled: "false" edc.iam.sts.oauth.token.url: "http://identityhub.provider.svc.cluster.local:7084/api/sts/token" edc.iam.sts.oauth.client.id: "did:web:identityhub.provider.svc.cluster.local%3A7083:provider" edc.iam.sts.oauth.client.secret.alias: "provider-participant-sts-client-secret" diff --git a/k8s/provider/application/controlplane-seed.yaml b/k8s/provider/application/controlplane-seed.yaml index 4744ffc32..dbf7eb638 100644 --- a/k8s/provider/application/controlplane-seed.yaml +++ b/k8s/provider/application/controlplane-seed.yaml @@ -58,6 +58,7 @@ spec: POLICIES_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/policydefinitions" CONTRACTDEFS_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/contractdefinitions" DATAPLANE_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v4beta/dataplanes" + CEL_URL="http://controlplane.provider.svc.cluster.local:8081/api/mgmt/v5alpha/celexpressions" # Posts to the management API, treating 409 (already exists) as success. post() { @@ -86,7 +87,7 @@ spec: "@id": "asset-1", "@type": "Asset", "properties": { - "description": "This asset requires Membership to view and negotiate." + "description": "This asset requires Membership to view and Manufacturer (part_types=non_critical) to negotiate." }, "dataAddress": { "@type": "DataAddress", @@ -111,7 +112,7 @@ spec: "@id": "asset-2", "@type": "Asset", "properties": { - "description": "This asset requires Membership to view and SensitiveData credential to negotiate." + "description": "This asset requires Membership to view and Manufacturer (part_types=all) to negotiate." }, "dataAddress": { "@type": "DataAddress", @@ -124,6 +125,52 @@ spec: echo "✓ Asset 2 done" + echo "================================================" + echo "Step 1: Create Membership CEL Expression " + echo "================================================" + + post "${CEL_URL}" "POST" '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CelExpression", + "@id": "membership-cel", + "leftOperand": "MembershipCredential", + "description": "Expression for evaluating membership credential", + "scopes": [ + "catalog", + "contract.negotiation", + "transfer.process" + ], + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t.contains('\''MembershipCredential'\''))).exists(c, c.credentialSubject.exists(cs, timestamp(cs.membershipStartDate) < now))" + }' + + echo "✓ Membership CEL Expression done" + + echo "" + echo "================================================" + echo "Step 2: Create ManufacturerCredential CEL Expression" + echo "================================================" + + post "${CEL_URL}" "POST" '{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "CelExpression", + "@id": "manufacturer-cel", + "leftOperand": "ManufacturerCredential.part_types", + "description": "Expression for evaluating manufacturer credential", + "scopes": [ + "catalog", + "contract.negotiation", + "transfer.process" + ], + "expression": "ctx.agent.claims.vc.filter(c, c.type.exists(t, t.contains('\''ManufacturerCredential'\''))).exists(c, c.credentialSubject.exists(cs, cs.part_types == this.rightOperand))" + }' + + echo "✓ ManufacturerCredential CEL Expression done" + + echo "" echo "================================================" echo "Step 3: Create Membership Policy" @@ -169,9 +216,9 @@ spec: { "action": "use", "constraint": { - "leftOperand": "ManufacturerCredential", + "leftOperand": "ManufacturerCredential.part_types", "operator": "eq", - "rightOperand": "active" + "rightOperand": "non_critical" } } ] @@ -190,7 +237,7 @@ spec: "https://w3id.org/edc/connector/management/v2" ], "@type": "PolicyDefinition", - "@id": "require-sensitive", + "@id": "require-manufacturer-all", "policy": { "@type": "Set", "permission": [], @@ -199,9 +246,9 @@ spec: { "action": "use", "constraint": { - "leftOperand": "ManufacturerCredential", + "leftOperand": "ManufacturerCredential.part_types", "operator": "eq", - "rightOperand": "active" + "rightOperand": "all" } } ] @@ -245,7 +292,7 @@ spec: "@id": "sensitive-only-def", "@type": "ContractDefinition", "accessPolicyId": "require-membership", - "contractPolicyId": "require-sensitive", + "contractPolicyId": "require-manufacturer-all", "assetsSelector": { "@type": "Criterion", "operandLeft": "https://w3id.org/edc/v0.0.1/ns/id", diff --git a/launchers/controlplane/build.gradle.kts b/launchers/controlplane/build.gradle.kts index 476ae7ed6..92c992622 100644 --- a/launchers/controlplane/build.gradle.kts +++ b/launchers/controlplane/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { runtimeOnly(project(":extensions:data-plane-registration")) runtimeOnly(libs.edc.api.cel.v5) runtimeOnly(libs.edc.core.cel) + runtimeOnly(libs.edc.decentralized.claims.cel) runtimeOnly(libs.edc.cel.store.sql) runtimeOnly(libs.edc.bom.controlplane) runtimeOnly(libs.edc.api.secrets) diff --git a/launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java b/launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java deleted file mode 100644 index 3b2b976af..000000000 --- a/launchers/identity-hub/src/test/java/org/eclipse/edc/demo/dcp/JwtSigner.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.demo.dcp; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import org.eclipse.edc.iam.did.spi.document.DidDocument; -import org.eclipse.edc.keys.keyparsers.PemParser; -import org.eclipse.edc.security.token.jwt.CryptoConverter; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.time.Instant; -import java.util.Date; -import java.util.Map; -import java.util.stream.Stream; - -import static org.mockito.Mockito.mock; - -/** - * Use this test to read a verifiable credential from the file system, and sign it with a given private key. You will need: - *
    - *
  • A JSON file containing the VC
  • - *
  • A public/private key pair in either JWK or PEM format
  • - *
- */ -@SuppressWarnings("NewClassNamingConvention") -public class JwtSigner { - - public static final String ISSUER_PRIVATE_KEY_FILE_PATH = System.getProperty("user.dir") + "/../../deployment/assets/issuer_private.pem"; - public static final String ISSUER_PUBLIC_KEY_FILE_PATH = System.getProperty("user.dir") + "/../../deployment/assets/issuer_public.pem"; - public static final File ISSUER_DID_DOCUMENT_LOCAL = new File(System.getProperty("user.dir") + "/../../deployment/assets/issuer/did.docker.json"); - public static final File ISSUER_DID_DOCUMENT_K8S = new File(System.getProperty("user.dir") + "/../../deployment/assets/issuer/did.k8s.json"); - public static final String DATASPACE_ISSUER_DID_LOCAL = "did:web:localhost%3A9876"; - public static final String DATASPACE_ISSUER_DID_K8S = "did:web:dataspace-issuer"; - private final ObjectMapper mapper = new ObjectMapper(); - - @ParameterizedTest - @ArgumentsSource(InputOutputProvider.class) - void generateJwt(String rawCredentialFilePath, File vcResource, String did, String issuerDid, File issuerDidDocument) throws JOSEException, IOException { - - var header = new JWSHeader.Builder(JWSAlgorithm.EdDSA) - .keyID(issuerDid + "#key-1") - .type(JOSEObjectType.JWT) - .build(); - - - var credential = mapper.readValue(new File(rawCredentialFilePath), Map.class); - - var claims = new JWTClaimsSet.Builder() - .audience(did) - .subject(did) - .issuer(issuerDid) - .claim("vc", credential) - .issueTime(Date.from(Instant.now())) - .build(); - - // this must be the path to the Credential issuer's private key - var privateKey = (PrivateKey) new PemParser(mock()).parse(readFile(ISSUER_PRIVATE_KEY_FILE_PATH)).orElseThrow(f -> new RuntimeException(f.getFailureDetail())); - var publicKey = (PublicKey) new PemParser(mock()).parse(readFile(ISSUER_PUBLIC_KEY_FILE_PATH)).orElseThrow(f -> new RuntimeException(f.getFailureDetail())); - - // sign raw credentials with new issuer public key - var jwt = new SignedJWT(header, claims); - jwt.sign(CryptoConverter.createSignerFor(privateKey)); - - // replace the "rawVc" field in the VC resources file, so that it gets seeded to the database - var content = Files.readString(vcResource.toPath()); - var updatedContent = content.replaceFirst("\"rawVc\":.*,", "\"rawVc\": \"%s\",".formatted(jwt.serialize())); - Files.write(vcResource.toPath(), updatedContent.getBytes()); - - // update issuer DID document with new public key - var issuerJwk = CryptoConverter.createJwk(new KeyPair(publicKey, null)); - var didDoc = mapper.readValue(issuerDidDocument, DidDocument.class); - - var issuerPk = didDoc.getVerificationMethod().get(0).getPublicKeyJwk(); - issuerPk.clear(); - issuerPk.putAll(issuerJwk.toPublicJWK().toJSONObject()); - Files.write(issuerDidDocument.toPath(), mapper.writeValueAsBytes(didDoc)); - } - - private String readFile(String path) { - try { - return Files.readString(Paths.get(path)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static class InputOutputProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext extensionContext) { - return Stream.of( - - // PROVIDER credentials, K8S and local - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/provider/membership_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/provider/membership-credential.json"), - "did:web:provider-identityhub%3A7083:bob", DATASPACE_ISSUER_DID_K8S, ISSUER_DID_DOCUMENT_K8S), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/provider/dataprocessor_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/provider/dataprocessor-credential.json"), - "did:web:provider-identityhub%3A7083:bob", DATASPACE_ISSUER_DID_K8S, ISSUER_DID_DOCUMENT_K8S), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/provider/unsigned/membership_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/provider/membership-credential.json"), - "did:web:provider-identityhub%3A7083:bob", DATASPACE_ISSUER_DID_LOCAL, ISSUER_DID_DOCUMENT_LOCAL), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/provider/unsigned/dataprocessor_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/provider/dataprocessor-credential.json"), - "did:web:provider-identityhub%3A7083:bob", DATASPACE_ISSUER_DID_LOCAL, ISSUER_DID_DOCUMENT_LOCAL), - - // CONSUMER credentials, K8S and local - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/consumer/membership_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/consumer/membership-credential.json"), - "did:web:consumer-identityhub%3A7083:alice", DATASPACE_ISSUER_DID_K8S, ISSUER_DID_DOCUMENT_K8S), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/consumer/dataprocessor_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/k8s/consumer/dataprocessor-credential.json"), - "did:web:consumer-identityhub%3A7083:alice", DATASPACE_ISSUER_DID_K8S, ISSUER_DID_DOCUMENT_K8S), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/consumer/unsigned/membership_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/consumer/membership-credential.json"), - "did:web:consumer-identityhub%3A7083:alice", DATASPACE_ISSUER_DID_LOCAL, ISSUER_DID_DOCUMENT_LOCAL), - - Arguments.of(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/consumer/unsigned/dataprocessor_vc.json", - new File(System.getProperty("user.dir") + "/../../deployment/assets/credentials/local/consumer/dataprocessor-credential.json"), - "did:web:consumer-identityhub%3A7083:alice", DATASPACE_ISSUER_DID_LOCAL, ISSUER_DID_DOCUMENT_LOCAL) - - ); - } - } -} diff --git a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSource.java b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSource.java index b659fad83..0e06a15ea 100644 --- a/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSource.java +++ b/launchers/issuerservice/src/main/java/org/eclipse/edc/issuerservice/seed/attestation/manufacturer/ManufacturerAttestationSource.java @@ -30,7 +30,7 @@ public Result> execute(AttestationContext context) { return Result.success(Map.of( "contractVersion", contractVersion, - "component_types", "all", + "component_types", "non_critical", "since", Instant.now().toString(), "id", context.participantContextId() )); diff --git a/resources/data_setup.drawio b/resources/data_setup.drawio deleted file mode 100644 index 131053abf..000000000 --- a/resources/data_setup.drawio +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/data_setup.png b/resources/data_setup.png deleted file mode 100644 index 1d51cfe4261d75c69bb67298c6a334892db58dbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97811 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�%EC4hhzmu_74c3s7oC;H|*`B zyK+2MnKqGB@oN2t?*Mn`7-ti$)En)ydZ-rc1m;6+B z-&xehbl}mScJZy0q2iZFPoWk_kKXP6T)#-(vt3FdVlE&vR%%H{P!9ljbz*TZ?8^>V z%?amk)zb}pwa3CMkjvc(9%*7x#R7jwmRN~sIXtzK^*tCu)+&*t=+J1{IIKCwHO}{a zn;>#V=t3Vj5Rn=XPOMV!D<;*Mx@?eZX9Py3BZ249h*}^pYw!;Q|~rvCp@T5pj^(gy~%x(;5H=e zYvt#KrRvvvwq&`!7ic%tww6PwMJ7022S6pX&wSY1R&5q+eIYq_gmj{`yYqwG` zS)i0`~Ujb9w$?$bQLa_>QQ_ zQzbe;4N{MhI}E-Aai8VXFz|OL{qA!u7ch8g!(Sn^lp(8K;-(Fo>h-Tb zE(}BPvUx9}qitikH@<}E<3P2DI3W{V*dU%-9IrxqAGg-jEG|BgKY_P=v@x4eky zv!e$bR!pD^=|^TxGoZJaxxtdi&{*ZM8#?ykq-Vh~kjB3ULM@xRFOb40NwL_t-mM--@Q*bF zKq{=MFmX-*5bP=ds|JRDF|WYmK4u#5E*x?+*4Ys^4_yD*vAZbB?z$-oraD#91EP&( z9vArViOYU9uqglUT<+vx*eZD5tSm>HMj2$Ez#wKq6aNvEMHvr_10_23k#m_0KsmyJa&rW;&*__pKkGg zIMreR#kxt`kUR&5&N8J%&#?ndwRqalbDaWB;?ZXC;40GKgQf2dUo$muO3_}fXh?@Nna;}ze>#>(ftElPvDSU5wD zEkWP_cyC;ye8wTT%`W(X7Nb&i+1&8eLV$e_TmxZc*{7Emk7X+PU(i;{ik$lxl%>G; zCSva6#jca$HoJ;Blmjlk1xcqa0AmcP%?%0o_eX(bmiNmGhPaH(jk0gpXQw2CQPK-@ zpKf@A%*ssoQix0FueekD2e8!lPbR2sglJ`k?l_ynM_{&vR{rJA%T z?(ZavWkdPNVo1kRc0omVISfEDXQcbO^&PB*edu`zVq)t6iO}48C6bAeLjW2{L+~&v z55Xt*!CzOu0UlN-2@R8P=iu{pkiV zt$V%-B}33cgMnXty3pv8lkB=cig*}6w;`+bmo)6WhDguBeDEylp6wcM4VtH&XypgLhfNNl9J4n;WY-affHfdkq(s z3~vnv6)+jD>h5`f25rBH+q}*OZt2}Z-H@Jeke@n7!vNJaB$58I%_0cIqnT%&y{Zok z1Dxy)rRt&zfVjjzz(li*wtq%V1HpS+0)WYKKV9Cy6WRU%^|?#g=hNsrb(=)gzC>_= zacZ%WY@=)%8dN9RWM^)nZqR=!-b@`=2TnQ^7T_3C*2ti@|#0MXm7a*~Uh zBcB{mL$x`4KZ5Lgt=}=GUa6?1a0P6?-h31y4P?6`eSt@zpy60LdiS;?lRPdyA1SJR z^F24@Jq-)fMA5_9fy@E(iED7+O>^!TMKZ>v?dIqc+DY1Ne17LIciouLad(&zWLDf5 zXvJ{SGXgz>v)ry}(Gxqe_5jT5dg;dtnIpZWj~P`^dSN$XaGiKBiKZBg5 z00JR79yiPU89<#P_urL;y|wUj9|sBJyDA9*>Lc;JPqOWlIl3L&8191HI-Oe%NY1|X zy4MBecl(T#KAh0oOq-0^uiF9y+OwIe-daPGZq~-C_~v8L*E5U4kD|3VNOqyN*d6$I zK8V989K})dygjs6L^gxhl!K11rtU$gv`a8dc0M7@+2zJmjvJK(U^*%M!>MPou5 zKa_oYL?>B%LJMH%x4q&7?V7<{t`1t)KHI&Xw9gR88WqV7NY~yXfV`Q~%ggb2wlSXZb z8T|?jPDy+mI{XG-bCsx7**K}tgdj^1GmG!(e1G)s{qlG=t)hirKU0fdj&@cshooRLqhe%@1bO~; zknqsE>!U~;T3;otTR^@Dr%$&%mUUcK0aQ4=l(bJ_nvfAKaCB8{>X6oYBTy~P^AaW8 zyL({zR9Ld@dBxFs8Qnw)$zjWUJ<1@eUe|&0T2P1}O*fqlDjc*)ClHMW+l;0`i9o}n z6n1fIOf9I(Yr$T62eLyaa}sR}z!@1?O=4^+{pIqPm}7nA0HwA;R}~o!73H!TC2=%K z#ZXW3ggYGV_KzIYlX+rW)BW6_`A|Re=z9?cHls!0$!AJc=Epv0xDEJ{PdN$m=bPLx z*$6?f4rA>_j$X~xMu<530Y!ww_B4_KYM8CT07Eqsb_UH_<(T4Z??T1V7Ar43wy}Kr z>9!lEU87x}2UaPv_jZKW1Um8!a>UU$GE47QV5d{bFLT|&*B8(^>241B-cZ@djY3?q z27*2sEjb_I`r}*ILq!nOe*m1tvb}l za)@V8&`34wAS()?%JQCKVbA33KJ)}MgdU-f`mBr8U7Q^zm# z#`fu$2ild51*!FEpG)POpL0CxYBbs$bfHI2IN{PXX%0Xqo}4-5&$Bqvsss`yyVU!G zJ6Jd$nm7-T2lXF6N$mp45XOCC_;)c{Ih9E9 zCh47>whHwIGAhR6`RM%C=48ZxpqVQ_E1EjjR+5~&F&&+H)Z?l@NJ__LLbywB`Bttn z;2cUYQxAtftira%z7tUs zoSD}ja?w6ao-8ciUq$-Xq*~b>xQB+rC(*IYgP7QJ-5K;V#uIJTEKG^do%;qr>D!$> zJ77C41#T9dV9>{hLeZ@(6O#)J0E&2=-gQIqa!BQ@G@p^;-9-5i0HGrG3HiVqB>BE5 zHicRqFbd3?E%*v&u9SI1WBM(0|L2}B^n-#MWr9AMsna|z(q|BM$souiuP7F)YKiqdIhD)5J^g#DOq_b;4Je%#MW)x>OrebwvwzsWX*0E8!Bz*6 zjY@m;(fwB2sZ=U%`#MEGp3yr~95ibEEO*!npo^iY+8gOuk7#;8^%RF(%-IvP>HM!v zb56=3$lSUu`$L#Sr!t3VBq^9DB}#9fzJ1yT7H+i*P0dElJ(bjFc{B7Y5A0}?;^!cF zY54(k+$B)I*FG(TEEsa$Ed!2gQ6Z>sVkgz*8l0m?MjoS_drLHJdNUwfP~33tD9?-{U58K=-3hT^ zi1$dPt&s}8Mw86ReVoxpdFA4sy?3pd>8d*W;#t_W&tWN!8HtE7L$e&Ty&zdWhpEOx z6Z6y04ScFQV{WT$ao|MIG<*q>;~Z|g;iD3h$kD@*Pv^UIo9wAfOI490by2H~CG5i# zfRe1MsbWOB9nbc0F6i0ypU?Ud$p-@0*Q{nzbGSj)mJ}A->Ms`8^SE2AiP>s9<(Y@40wo~IC4x#TkUJE|#d>n28rjAj zb+L--!_M;Tx6UR6A9&aGVa8}lLO_PBgB#pBK*_Q+!@RBuSqfBI6YekYkw)fJM6+@F zCKi>RD%KuR`{p>pr+%mQ^M_IF*K*@4n}d3pMJpw28hekO6My4Y`#GmhJ%cIByE*=4 z<1?zpLJu<)(<7S_?5`DFJ-8dLXdLRgDZnTTyJd#Q*j-R_>p5{{WdTkQRe71t$%Bl% z7vF1YHCsAkvCsa70iHoL5Cb~Qd0JU#gkqv$mSzH|z@8)6%ur#rDy-x=KdT7U`mMw2sYB?)1vg zdg!+G!BscWkxG7G(%f2l>Hbn{&i<;(torbV2FV~R>F>UhH_eaS#8z)r z!%}_&VKpMSaBMd);gbr#IPSKAoNQ3xs*z(Hsvv~O*kU8kmj1f+YE>$Y{H!mdylnd1 z>txSkd@S$DJT|`OZ?2h33M6&2bAnz2Syq0?HwWo(U3n!6{ZLI_iXA!_648hm7LSp! zCrtV%GL<~lgdN(Bj&v~xK<@4ZIM!XRUI_7n2Qtm}iPr2&v%;qJp_1iVc+$oa9cc{2 zhO!=JnRs$0AdED4j*K)N((t6cgh5d@c4Qnb*T+owvr~Su1VNH;SjxH6yju!d`MV4e zebWpg3bj33rfEt2x5sQ2jlQJ&$UE%Fd`_y(_;m5!>(<{?v5Ssk8(<^t46!B}$}-hu z8149c)<}sX+x)2}nHj%>S`y1@W>dO_tYX*a3Doh4uv?(^PtO_;S z!6armA~g+5;T1Tj?F;=L6s7T z$UCi~s@3|Tg#SS5zLORD-PJ|0lnjS~zvWz0e@DZ!C%ie0?s;b7W7M{Bzs*SACH zX&oB|QKgk4B{fFK?w8Uy{?J2tq~jk;Q=pqx(Qv4Ks^DtcTsf?)fh*y}y+`%B3N#V%iFVWgQ~DqZI`!JUT%zdT@Ptv?vcmk^0W zXgw@8P}I>q!hybQ+fD+~(vJj9TO3~`WAcQ_N+Vgak4vY%p-x~oY8i6SN9pwlPI1>C zd@R8h;kXNgn)At7PjAnjVKobVhk5hp%=5(tI>8Eq1vT9^9notHJcA>3gVRrCfc*@- z#QyrE+O+Wo-5r|bf!x?;SQJxEQqKORUvQ|?RvPlz+03VYS8!HEPXQEfeH6a-4G$%v z_S@hL#wJc*>#p$07l&Ru!JTUQ>yL{^P!7wxDd|1YDB(X0*Ue-XtsE>6($~QxS{#qw zy{%tbgqLZyL0?X@N@!_w<~|;D^!rx@Oy~sfeE;-K@eakGhfPxXC|s_90)V26`98%0 zY5c(#xq*<$>(m#91f@^TYeh=~6&Ky4(_o&@G9&RMOZ9kNYWY>YXlHk!Vz0Na2V@5n zT1urw2J)G{R*h3lUd18NW`^*g#_rw6EJksgD}jo**sbntgA@n+CDA|-fi)(NkA&i2 zA_mTNFuQ@(=j)LGJ;g3R6P-414jJbo=V|!&&{CY<{+M6C+^1ZZZvs?w3ko4qeX>pn z*+)?23&JEpL3M@RRuS69^x!NhH56l`+Ds3b!4u6gO6)nuBot*!W|K%9bNL`=8f*U+ zK`)leXg#kOCN;e`=|lczQoodIZ$5Rn+)piu{(;IUu!&fw69tNWOBZ29(<;4R?GRdX zg$O1GkeiMhUC@Q)7wQDO$)C!$62~Rr#+d4A2YtruD85<(YQrks;Qp0GfAzQ^JCnzhj2%&Xl zP8gCXHXu=BDQ2pKOLWoup&KEVZCYhOr$K2jgv)eRTj#zoY^8dv)L6rPnCBEpVtX0)TOcb?OZk14c}uf2j#m=-cY?(oC94dJX|DDof%*emt3Msbu`-$q(?^v1E= z)iDX-ALYN{Em*zX`|80Z2EP zeP4J{DX87_Tr^VEt|phKeb2qWPoN<}o>E5_c{6NtRLw}oZ97nZCw`8k!GD}X-pH(? zFmiG3WN+_aZq&PB(pw*yq7HUbQ?+cDISIAYeD3sbv?`?*#KfKkwRDS_OYcBIMw}8ysrkAq6HfOD7;4IgTOjAjlAClXKi>ss~k`Dwf#_ z@+ZxFZb+TWEq)e=zU^|YE_LVt+v`F#!mX1GNN$Z1={rg)FJ=d2gh3^>8slugH0<3M zV9&IL5yAxh8qSTc+)Y~80p&UrK&}d^3TeA&g5AAs>3*s4n*?t4zP9nXQ)~<_uF!-VF~}n|dp28vde66!^P}RWWSNrs z;zvh|YWco(*oTdNu#Z{{oCfx(v^-+|jmre$IC;eRQE&^V+4NY8!6K?u+`4#ztsbVj zckHNT8h`WGohQ=>e)-Hk$kv0aCigwSeLIV-`yhY$i_>$i1VWkwlL?A(JIYCwL0y1) zIjrtm_|C3F;EwTWnaxGfC$y%LMkr?(>JCUbR8vEDldM_E&TxQQaOSGb#u&INOp}6% zl*1@ApKW_u^bl18bk*Sui)x9QlV9Q0w%4HS>DnezCUTBGn~KN?!O^DAwVQqVfHli> zN^otzdG_m3Yu4t=Ni*Ic2Owt0>Yxc=A%QM|K!1?^5l@fx+i#dDFDD&a{Mu3(6_0AW zs@OSY2l>g=0wg;c_a)I&bn-KEKAQP-JlFA?jA*|I$jEYft++spVs9_qySIqQ_d=M`%UyoWp{9IX;VEc zoAc3C|D=lZQfVY91%)-2tn{rfSSGXAry3c#+dzwZgaTlT1wxA?_~^BlB+In`@u+yI z#W(%amW#V+XAKdH=g!qFfYKW0(imzX#*(yc-)gvhinC4Q6rbTC8#7(7zt*DN(CxEc zVF-3?-LX*RXXNz+sC<>3=BA47Fgj=!n|96tOk7sl0o=VB191PX;+^3M9vqDzmkl3| z8KOOMiSe~n5wOLb{;@jX9!aV_UdCF K?K$zu-Y>htsR3bURfb&Tst_c#&b*5CDf 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

dG(EzcU}tkA|(8jMP>>Y9Gd?ydk@O#j!!fm_9 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 e@M}2D^q=3fRD0EQP8&-GAn ds.getId().equals("asset-2")) + var oid = cat.getDatasets().stream().filter(ds -> ds.getId().equals("asset-1")) .flatMap(ds -> ds.getPolicies().stream()) .map(CatalogResponse.Offer::getId) - .findFirst().orElseThrow(() -> new AssertionError("No offer found for asset-2")); + .findFirst().orElseThrow(() -> new AssertionError("No offer found for asset-1")); offerId.set(oid); }); @@ -216,7 +212,6 @@ void transferData_hasPermission_shouldTransferData() { assertThat(response).isNotEmpty(); } - @Disabled @DisplayName("Tests a failing End-to-End contract negotiation because of an unfulfilled policy") @Test void transferData_doesNotHavePermission_shouldTerminate() { @@ -226,78 +221,72 @@ void transferData_doesNotHavePermission_shouldTerminate() { .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .get(PROVIDER_MANAGEMENT_URL + "/api/management/v3/dataplanes") + .get(PROVIDER_MANAGEMENT_URL + "/api/mgmt/v4beta/dataplanes") .then() .statusCode(200) .log().ifValidationFails() .extract().body().jsonPath(); var state = jp.getString("state"); - assertThat(state).isEqualTo("[AVAILABLE]"); + assertThat(state).isEqualTo("[REGISTERED]"); }); System.out.println("Provider dataplane is online, fetching catalog"); - var emptyQueryBody = Json.createObjectBuilder() - .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/v0.0.1/ns/")) - .add("@type", "QuerySpec") + var catalogRequestBody = Json.createObjectBuilder() + .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/connector/management/v2")) + .add("@type", "CatalogRequest") + .add("counterPartyId", PROVIDER_ID) + .add("counterPartyAddress", "http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1") + .add("protocol", "dataspace-protocol-http:2025-1") + .add("querySpec", Json.createObjectBuilder().build()) .build(); var offerId = new AtomicReference(); // get catalog, extract offer ID await().atMost(TEST_TIMEOUT_DURATION) .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { - var jo = baseRequest() - .body(emptyQueryBody) - .post(CONSUMER_CATALOG_URL + "/api/catalog/v1alpha/catalog/query") + var res = baseRequest() + .body(catalogRequestBody) + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/catalog/request") .then() - .log().ifError() + .log().ifValidationFails() .statusCode(200) - .extract().body().as(JsonArray.class); - - var offerIdsFiltered = jo.stream().map(jv -> { - - var expanded = jsonLd.expand(jv.asJsonObject()).orElseThrow(f -> new AssertionError(f.getFailureDetail())); - var cat = transformerRegistry.transform(expanded, Catalog.class).orElseThrow(f -> new AssertionError(f.getFailureDetail())); - return cat.getDatasets().stream().filter(ds -> ds instanceof Catalog) // filter for CatalogAssets - .map(ds -> (Catalog) ds) - .filter(sc -> sc.getDataServices().stream().anyMatch(dataService -> dataService.getEndpointUrl().contains("provider-qna"))) // filter for assets from the Q&A Provider - .flatMap(c -> c.getDatasets().stream()) - .filter(dataset -> dataset.getId().equals("asset-2")) // we should not be allowed to negotiation for this asset! - .map(Dataset::getOffers) - .map(offers -> offers.keySet().iterator().next()) - .findFirst() - .orElse(null); - }).toList(); - assertThat(offerIdsFiltered).hasSize(1); - var oid = offerIdsFiltered.get(0); - assertThat(oid).isNotNull(); + .extract().body().as(JsonObject.class); + + // todo: parse asset offer ID, parse JSON + var cat = objectMapper.readValue(res.toString(), CatalogResponse.class); + var oid = cat.getDatasets().stream().filter(ds -> ds.getId().equals("asset-2")) + .flatMap(ds -> ds.getPolicies().stream()) + .map(CatalogResponse.Offer::getId) + .findFirst().orElseThrow(() -> new AssertionError("No offer found for asset-2")); offerId.set(oid); }); System.out.println("Initiate contract negotiation"); // initiate negotiation - var negotiationRequest = TestUtils.getResourceFileContentAsString("negotiation-request.json") + var negotiationRequest = TestUtils.getResourceFileContentAsString("negotiation-request_invalid.json") .replace("{{PROVIDER_ID}}", PROVIDER_ID) .replace("{{PROVIDER_DSP_URL}}", PROVIDER_DSP_URL) - .replace("{{OFFER_ID}}", offerId.get()) - .replaceFirst("\"odrl:rightOperand\": \"processing\"", " \"odrl:rightOperand\": \"sensitive\""); + .replace("{{OFFER_ID}}", offerId.get()); var negotiationId = baseRequest() .body(negotiationRequest) - .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations") + .post(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/contractnegotiations") .then() .log().ifError() .statusCode(200) .extract().body().jsonPath().getString("@id"); assertThat(negotiationId).isNotNull(); + System.out.println("Wait until negotiation is TERMINATED"); + //wait until negotiation is TERMINATED await().atMost(TEST_TIMEOUT_DURATION) .pollDelay(TEST_POLL_DELAY) .untilAsserted(() -> { var jp = baseRequest() - .get(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations/" + negotiationId) + .get(CONSUMER_MANAGEMENT_URL + "/api/mgmt/v4beta/contractnegotiations/" + negotiationId) .then() .statusCode(200) .extract().body().jsonPath(); diff --git a/tests/end2end/src/test/resources/negotiation-request.json b/tests/end2end/src/test/resources/negotiation-request.json index 7e11ef2d0..b24cbbe0f 100644 --- a/tests/end2end/src/test/resources/negotiation-request.json +++ b/tests/end2end/src/test/resources/negotiation-request.json @@ -15,12 +15,12 @@ "obligation": { "action": "use", "constraint": { - "leftOperand": "ManufacturerCredential", + "leftOperand": "ManufacturerCredential.part_types", "operator": "eq", - "rightOperand": "active" + "rightOperand": "non_critical" } }, - "target": "asset-2" + "target": "asset-1" }, "callbackAddresses": [] } \ No newline at end of file diff --git a/tests/end2end/src/test/resources/negotiation-request_invalid.json b/tests/end2end/src/test/resources/negotiation-request_invalid.json new file mode 100644 index 000000000..dca03b831 --- /dev/null +++ b/tests/end2end/src/test/resources/negotiation-request_invalid.json @@ -0,0 +1,26 @@ +{ + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "ContractRequest", + "counterPartyAddress": "{{PROVIDER_DSP_URL}}", + "counterPartyId": "{{PROVIDER_ID}}", + "protocol": "dataspace-protocol-http:2025-1", + "policy": { + "@type": "Offer", + "@id": "{{OFFER_ID}}", + "assigner": "{{PROVIDER_ID}}", + "permission": [], + "prohibition": [], + "obligation": { + "action": "use", + "constraint": { + "leftOperand": "ManufacturerCredential.part_types", + "operator": "eq", + "rightOperand": "all" + } + }, + "target": "asset-1" + }, + "callbackAddresses": [] +} \ No newline at end of file From fe5b9ff0f182aa708d107c33c4092f7190e54b91 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 30 Mar 2026 12:42:56 +0200 Subject: [PATCH 22/22] style/cosmetics --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fb69ee998..a663790aa 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ It must be stated in the strongest terms that this is **NOT** a production grade production-grade developments be based on it. [Shortcuts](#10-other-caveats-shortcuts-and-workarounds) were taken, and assumptions were made that are potentially invalid in other scenarios. -It merely is a playground for developers wanting to kick the tires in the EDC and DCP space, and its purpose is to +It is merely a playground for developers wanting to kick the tires in the EDC and DCP space, and its purpose is to demonstrate how DCP works to an otherwise unassuming audience. ### 2.1 Version stability and backwards compatibility guarantees @@ -88,7 +88,7 @@ anything, then you'll get the absolute latest version of MVD. This is suitable f frequently and with the occasional breakage. The upshot is that this branch will always contain the latest features and fixes of all upstream components. -> We have monitoring systems im place that inform us about broken builds. No need to raise issues about this. +> We have monitoring systems in place that inform us about broken builds. No need to raise issues about this. More conservative developers may fall back to [releases of MVD](https://github.com/eclipse-edc/MinimumViableDataspace/releases) that use release versions of all @@ -96,11 +96,11 @@ upstream components. If this is you, then remember to check out the appropriate Either download the ZIP file and use sources therein, or check out the corresponding tag. -An MVD release version is typically created shortly after a release of the upstream components was released. +An MVD release version is typically created shortly after an upstream components release. ## 3. The Scenario -_In this example, we will see how two companies can share data using DSP and DCP. Each company deploys its own +In this example, we will see how two companies can share data using DSP and DCP. Each company deploys its own connector, IdentityHub, and base infrastructure. ### 3.1 Participants @@ -132,7 +132,7 @@ In this fictitious dataspace there are two types of VerifiableCredentials: the holder is allowed to manufacture. This is defined in the `"part_types"` field in the credential subject. The following variants exist: - `"part_type": "non_critical"`: means, the holder can manufacture non-critical parts - - `"part_type" : "all"`: means, the holder can manufacture everything including saftey critical parts + - `"part_type" : "all"`: means, the holder can manufacture everything including saftey-critical parts The information about the level of the holder is stored in the `credentialSubject` of the ManufacturerCredential. @@ -210,7 +210,7 @@ is required. It is assumed that the following tools are installed and readily av - Git - a POSIX compliant shell - Bruno (to comfortably execute REST requests) -- not needed, but recommended: Kubernetes monitoring tools like K9s +- optional, but recommended: Kubernetes monitoring tools like K9s All commands are executed from the **repository's root folder** unless stated otherwise via `cd` commands. @@ -307,7 +307,7 @@ The provider company has a control plane, a dataplane, plus an IdentityHub, a po In addition, there is the Issuer service, which is responsible for issuing Verifiable Credentials. -It is possible that pods need to restart a number of time before the cluster becomes stable. This is normal and +It is possible that pods need to restart a number of times before the cluster becomes stable. This is normal and expected. If pods _don't_ come up after a reasonable amount of time, it is time to look at the logs and investigate. Remote Debugging is possible, but Kubernetes port-forwards of port 1044 are necessary. @@ -410,14 +410,13 @@ similar to this: }, ``` -for the purposes of this tutorial we'll focus on the offers from the Provider's Q&A department, so the associated -service entry should be: +the associated service entry for the Provider's asset should be: ```json { "dcat:service": { // ... - "dcat:endpointUrl": "http://provider-qna-controlplane:8082/api/dsp", + "dcat:endpointUrl": "http://controlplane.provider.svc.cluster.local:8082/api/dsp/2025-1", "dcat:endpointDescription": "dspace:connector" // ... } @@ -435,7 +434,7 @@ into the `policy.@id` field of the `ControlPlane Management/Initiate Negotiation ```json //... "counterPartyId": "{{PROVIDER_ID}}", -"protocol": "dataspace-protocol-http", +"protocol": "dataspace-protocol-http:2025-1", "policy": { "@type": "Offer", "@id": "bWVtYmVyLWFuZC1wY2YtZGVm:YXNzZXQtMQ==:MThhNTgwMzEtNjE3Zi00N2U2LWFlNjMtMTlkZmZlMjA5NDE4", @@ -553,7 +552,7 @@ These are: ### 7.2 Regenerating key pairs Participant keys are dynamically generated by IdentityHub, so there is no need to pre-generate them. In fact, -everytime the dataspace is re-deployed and the seed jobs are executed, a new key pair is generated for each participant. +every time the dataspace is re-deployed and the seed jobs are executed, a new key pair is generated for each participant. To be extra-precise, the keys are regenerated when a new `ParticipantContext` is created. At runtime, a participant's key pair(s) can be regenerated and revoked using