From e2fbefc82f43f5685be09dfa7ae396dd7d3bfd43 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 12 Feb 2025 15:53:25 +0100 Subject: [PATCH] feat: implement StorageApi + validator --- .../bom/identityhub-base-bom/build.gradle.kts | 3 +- .../eclipse/edc/test/bom/BomSmokeTests.java | 4 + .../identityhub-remote-sts/build.gradle.kts | 2 +- .../presentation-api/build.gradle.kts | 0 .../api/PresentationApiExtension.java | 0 .../PresentationQueryValidator.java | 0 .../api/verifiablecredential/ApiSchema.java | 0 .../verifiablecredential/PresentationApi.java | 0 .../PresentationApiController.java | 0 ...rg.eclipse.edc.spi.system.ServiceExtension | 0 .../resources/presentation-api-version.json | 0 .../api/v1/PresentationApiControllerTest.java | 0 .../PresentationQueryValidatorTest.java | 0 .../storage-api/build.gradle.kts | 47 +++++ .../identityhub/api/StorageApiExtension.java | 136 ++++++++++++ .../identityhub/api/storage/ApiSchema.java | 90 ++++++++ .../identityhub/api/storage/StorageApi.java | 58 +++++ .../api/storage/StorageApiController.java | 82 ++++++++ .../CredentialMessageValidator.java | 75 +++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 ++ .../main/resources/storage-api-version.json | 8 + .../api/storage/StorageApiControllerTest.java | 199 ++++++++++++++++++ .../CredentialMessageValidatorTest.java | 99 +++++++++ .../dcp/spi/model/CredentialContainer.java | 19 ++ .../dcp/spi/model/CredentialMessage.java | 68 ++++++ .../postgres/CredentialDefinitionMapping.java | 2 +- .../schema/postgres/ParticipantMapping.java | 2 +- settings.gradle.kts | 3 +- .../spi/webcontext/IdentityHubApiContext.java | 1 + 29 files changed, 908 insertions(+), 5 deletions(-) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/build.gradle.kts (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/validation/PresentationQueryValidator.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/ApiSchema.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApi.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/main/resources/presentation-api-version.json (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java (100%) rename extensions/protocols/dcp/{ => dcp-identityhub}/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidatorTest.java (100%) create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/build.gradle.kts create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/StorageApiExtension.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/ApiSchema.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApi.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApiController.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidator.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/storage-api-version.json create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/storage/StorageApiControllerTest.java create mode 100644 extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidatorTest.java create mode 100644 extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialContainer.java create mode 100644 extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialMessage.java diff --git a/dist/bom/identityhub-base-bom/build.gradle.kts b/dist/bom/identityhub-base-bom/build.gradle.kts index 5e3185d15..9ae09852c 100644 --- a/dist/bom/identityhub-base-bom/build.gradle.kts +++ b/dist/bom/identityhub-base-bom/build.gradle.kts @@ -22,7 +22,8 @@ dependencies { runtimeOnly(project(":core:identity-hub-participants")) runtimeOnly(project(":core:identity-hub-keypairs")) runtimeOnly(project(":extensions:did:local-did-publisher")) - runtimeOnly(project(":extensions:protocols:dcp:presentation-api")) + runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:presentation-api")) + runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:storage-api")) runtimeOnly(project(":extensions:common:credential-watchdog")) runtimeOnly(project(":extensions:sts:sts-account-provisioner")) runtimeOnly(project(":extensions:api:identity-api:did-api")) diff --git a/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java b/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java index a95448064..b006dd823 100644 --- a/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java +++ b/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java @@ -66,6 +66,8 @@ class IdentityHub extends SmokeTest { put("edc.ih.iam.publickey.path", "/some/path/to/key.pem"); put("web.http.presentation.port", valueOf(getFreePort())); put("web.http.presentation.path", "/api/resolution"); + put("web.http.storage.port", valueOf(getFreePort())); + put("web.http.storage.path", "/api/storage"); put("web.http.identity.port", valueOf(getFreePort())); put("web.http.identity.path", "/api/identity"); put("web.http.version.port", valueOf(getFreePort())); @@ -95,6 +97,8 @@ class IdentityHubWithSts extends SmokeTest { put("edc.ih.iam.publickey.path", "/some/path/to/key.pem"); put("web.http.presentation.port", valueOf(getFreePort())); put("web.http.presentation.path", "/api/resolution"); + put("web.http.storage.port", valueOf(getFreePort())); + put("web.http.storage.path", "/api/storage"); put("web.http.identity.port", valueOf(getFreePort())); put("web.http.identity.path", "/api/identity"); put("web.http.accounts.port", valueOf(getFreePort())); diff --git a/e2e-tests/runtimes/identityhub-remote-sts/build.gradle.kts b/e2e-tests/runtimes/identityhub-remote-sts/build.gradle.kts index 816affb72..99c6bef5f 100644 --- a/e2e-tests/runtimes/identityhub-remote-sts/build.gradle.kts +++ b/e2e-tests/runtimes/identityhub-remote-sts/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { runtimeOnly(project(":core:identity-hub-keypairs")) runtimeOnly(project(":extensions:did:local-did-publisher")) runtimeOnly(project(":extensions:common:credential-watchdog")) - runtimeOnly(project(":extensions:protocols:dcp:presentation-api")) + runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:presentation-api")) runtimeOnly(project(":extensions:sts:sts-account-provisioner")) runtimeOnly(project(":extensions:sts:sts-account-service-remote")) runtimeOnly(project(":extensions:api:identity-api:did-api")) diff --git a/extensions/protocols/dcp/presentation-api/build.gradle.kts b/extensions/protocols/dcp/dcp-identityhub/presentation-api/build.gradle.kts similarity index 100% rename from extensions/protocols/dcp/presentation-api/build.gradle.kts rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/build.gradle.kts diff --git a/extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java diff --git a/extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/validation/PresentationQueryValidator.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/validation/PresentationQueryValidator.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/validation/PresentationQueryValidator.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/validation/PresentationQueryValidator.java diff --git a/extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/ApiSchema.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/ApiSchema.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/ApiSchema.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/ApiSchema.java diff --git a/extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApi.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApi.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApi.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApi.java diff --git a/extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java diff --git a/extensions/protocols/dcp/presentation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension diff --git a/extensions/protocols/dcp/presentation-api/src/main/resources/presentation-api-version.json b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/resources/presentation-api-version.json similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/main/resources/presentation-api-version.json rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/main/resources/presentation-api-version.json diff --git a/extensions/protocols/dcp/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java diff --git a/extensions/protocols/dcp/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidatorTest.java b/extensions/protocols/dcp/dcp-identityhub/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidatorTest.java similarity index 100% rename from extensions/protocols/dcp/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidatorTest.java rename to extensions/protocols/dcp/dcp-identityhub/presentation-api/src/test/java/org/eclipse/edc/identityservice/api/validation/PresentationQueryValidatorTest.java diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/build.gradle.kts b/extensions/protocols/dcp/dcp-identityhub/storage-api/build.gradle.kts new file mode 100644 index 000000000..7ae290a74 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/build.gradle.kts @@ -0,0 +1,47 @@ +/* + * 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 + * + */ + +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + api(project(":spi:identity-hub-spi")) + api(project(":spi:verifiable-credential-spi")) + api(project(":extensions:protocols:dcp:dcp-spi")) + api(libs.edc.spi.jsonld) + api(libs.edc.spi.jwt) + api(libs.edc.spi.core) + implementation(libs.edc.spi.validator) + implementation(libs.edc.spi.web) + implementation(libs.edc.spi.dcp) + implementation(libs.edc.lib.jerseyproviders) + implementation(libs.edc.lib.transform) + implementation(libs.edc.dcp.transform) + implementation(libs.jakarta.rsApi) + testImplementation(libs.edc.junit) + testImplementation(libs.edc.jsonld) + testImplementation(testFixtures(libs.edc.core.jersey)) + testImplementation(testFixtures(project(":spi:verifiable-credential-spi"))) + testImplementation(libs.nimbus.jwt) + testImplementation(libs.restAssured) +} + +edcBuild { + swagger { + apiGroup.set("storage-api") + } +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/StorageApiExtension.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/StorageApiExtension.java new file mode 100644 index 000000000..ddeb39c99 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/StorageApiExtension.java @@ -0,0 +1,136 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import org.eclipse.edc.iam.identitytrust.transform.from.JsonObjectFromPresentationResponseMessageTransformer; +import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer; +import org.eclipse.edc.identityhub.api.storage.StorageApiController; +import org.eclipse.edc.identityhub.api.validation.CredentialMessageValidator; +import org.eclipse.edc.identityhub.spi.verification.SelfIssuedTokenVerifier; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +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.EdcException; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.apiversion.ApiVersionService; +import org.eclipse.edc.spi.system.apiversion.VersionRecord; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.transform.transformer.edc.to.JsonValueToGenericTypeTransformer; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.jersey.providers.jsonld.ObjectMapperProvider; +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.io.IOException; +import java.util.stream.Stream; + +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_V_1_0_CONTEXT; +import static org.eclipse.edc.identityhub.api.StorageApiExtension.NAME; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage.CREDENTIAL_MESSAGE_TERM; +import static org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext.STORAGE; +import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; + +@Extension(value = NAME) +public class StorageApiExtension implements ServiceExtension { + + public static final String NAME = "Storage API Extension"; + private static final String API_VERSION_JSON_FILE = "storage-api-version.json"; + + @Configuration + private StorageApiConfiguration apiConfiguration; + @Inject + private TypeTransformerRegistry typeTransformer; + @Inject + private JsonObjectValidatorRegistry validatorRegistry; + @Inject + private WebService webService; + @Inject + private SelfIssuedTokenVerifier selfIssuedTokenVerifier; + @Inject + private JsonLd jsonLd; + @Inject + private TypeManager typeManager; + @Inject + private ApiVersionService apiVersionService; + @Inject + private PortMappingRegistry portMappingRegistry; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var contextString = STORAGE; + + portMappingRegistry.register(new PortMapping(contextString, apiConfiguration.port(), apiConfiguration.path())); + + validatorRegistry.register(DSPACE_DCP_NAMESPACE_V_1_0.toIri(CREDENTIAL_MESSAGE_TERM), new CredentialMessageValidator()); + + + var controller = new StorageApiController(validatorRegistry, typeTransformer, selfIssuedTokenVerifier, jsonLd); + webService.registerResource(contextString, new ObjectMapperProvider(typeManager, JSON_LD)); + webService.registerResource(contextString, controller); + + jsonLd.registerContext(DSPACE_DCP_V_1_0_CONTEXT, DCP_SCOPE_V_1_0); + + registerTransformers(DCP_SCOPE_V_1_0, DSPACE_DCP_NAMESPACE_V_1_0); + + registerVersionInfo(getClass().getClassLoader()); + } + + void registerTransformers(String scope, JsonLdNamespace namespace) { + var scopedTransformerRegistry = typeTransformer.forContext(scope); + scopedTransformerRegistry.register(new JsonObjectToPresentationQueryTransformer(typeManager, JSON_LD, namespace)); + scopedTransformerRegistry.register(new JsonValueToGenericTypeTransformer(typeManager, JSON_LD)); + scopedTransformerRegistry.register(new JsonObjectFromPresentationResponseMessageTransformer(namespace)); + } + + private void registerVersionInfo(ClassLoader resourceClassLoader) { + try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) { + if (versionContent == null) { + throw new EdcException("Version file not found or not readable."); + } + Stream.of(typeManager.getMapper() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .readValue(versionContent, VersionRecord[].class)) + .forEach(vr -> apiVersionService.addRecord("presentation", vr)); + } catch (IOException e) { + throw new EdcException(e); + } + } + + @Settings + record StorageApiConfiguration( + @Setting(key = "web.http." + STORAGE + ".port", description = "Port for " + STORAGE + " api context", defaultValue = 14141 + "") + int port, + @Setting(key = "web.http." + STORAGE + ".path", description = "Path for " + STORAGE + " api context", defaultValue = "/api/storage") + String path + ) { + + } + +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/ApiSchema.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/ApiSchema.java new file mode 100644 index 000000000..ba912348b --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/ApiSchema.java @@ -0,0 +1,90 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.storage; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +public interface ApiSchema { + @Schema(name = "ApiErrorDetail", example = ApiErrorDetailSchema.API_ERROR_EXAMPLE) + record ApiErrorDetailSchema( + String message, + String type, + String path, + String invalidValue + ) { + public static final String API_ERROR_EXAMPLE = """ + { + "message": "error message", + "type": "ErrorType", + "path": "object.error.path", + "invalidValue": "this value is not valid" + } + """; + } + + @Schema(name = "CredentialMessage", example = CredentialMessageSchema.CREDENTIALMESSAGE_EXAMPLE) + record CredentialMessageSchema( + @Schema(name = CONTEXT, requiredMode = REQUIRED) + Object context, + @Schema(name = TYPE, requiredMode = REQUIRED) + String type, + @Schema(name = "credentials", requiredMode = REQUIRED) + List credentials, + @Schema(name = "requestId", requiredMode = REQUIRED) + String requestId + ) { + + public static final String CREDENTIALMESSAGE_EXAMPLE = """ + { + "@context": [ + "https://w3id.org/dspace-dcp/v1.0/dcp.jsonld" + ], + "type": "CredentialMessage", + "credentials": [ + { + "credentialType": "MembershipCredential", + "payload": "", + "format": "jwt" + }, + { + "credentialType": "OrganizationCredential", + "payload": "", + "format": "json-ld" + } + ], + "requestId": "requestId" + } + """; + } + + + @Schema(name = "CredentialContainerSchema", example = CredentialContainerSchema.EXAMPLE) + record CredentialContainerSchema() { + + private static final String EXAMPLE = """ + { + "credentialType": "MembershipCredential", + "payload": "", + "format": "jwt" + } + """; + } +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApi.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApi.java new file mode 100644 index 000000000..b1109e3fe --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApi.java @@ -0,0 +1,58 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.storage; + + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.json.JsonObject; +import jakarta.ws.rs.core.Response; + +@OpenAPIDefinition( + info = @Info(description = "This represents the Storage API as per DCP specification. It serves endpoints to write VerifiableCredentials into storage.", title = "Storage API", + version = "1")) +@SecurityScheme(name = "Authentication", + description = "Self-Issued ID token containing an access_token", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT") +public interface StorageApi { + + @Tag(name = "Storage API") + @Operation(description = "Writes a set of credentials into storage", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ApiSchema.CredentialMessageSchema.class))), + responses = { + @ApiResponse(responseCode = "2xx", description = "The credentialMessage was successfully processed and stored"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "401", description = "No Authorization header was given.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " + + "calls for a broader credentialMessage scope than the granted scope in the auth token", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))) + + } + ) + Response storeCredential(String participantContextId, JsonObject credentialMessage, String authHeader); +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApiController.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApiController.java new file mode 100644 index 000000000..8d617dcc6 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/storage/StorageApiController.java @@ -0,0 +1,82 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.storage; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage; +import org.eclipse.edc.identityhub.spi.verification.SelfIssuedTokenVerifier; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage.CREDENTIAL_MESSAGE_TERM; +import static org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextId.onEncoded; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/v1alpha/participants/{participantContextId}/credentials") +public class StorageApiController implements StorageApi { + + private final JsonObjectValidatorRegistry validatorRegistry; + private final TypeTransformerRegistry transformerRegistry; + private final SelfIssuedTokenVerifier selfIssuedTokenVerifier; + private final JsonLd jsonLd; + + public StorageApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, + SelfIssuedTokenVerifier selfIssuedTokenVerifier, JsonLd jsonLd) { + this.validatorRegistry = validatorRegistry; + this.transformerRegistry = transformerRegistry; + this.selfIssuedTokenVerifier = selfIssuedTokenVerifier; + this.jsonLd = jsonLd; + } + + + @POST + @Override + public Response storeCredential(@PathParam("participantContextId") String participantContextId, JsonObject credentialMessageJson, @HeaderParam(AUTHORIZATION) String authHeader) { + if (authHeader == null) { + throw new AuthenticationFailedException("Authorization header missing"); + } + var authtoken = authHeader.replace("Bearer", "").trim(); + credentialMessageJson = jsonLd.expand(credentialMessageJson).orElseThrow(InvalidRequestException::new); + validatorRegistry.validate(DSPACE_DCP_NAMESPACE_V_1_0.toIri(CREDENTIAL_MESSAGE_TERM), credentialMessageJson).orElseThrow(ValidationFailureException::new); + var protocolRegistry = transformerRegistry.forContext(DCP_SCOPE_V_1_0); + + participantContextId = onEncoded(participantContextId).orElseThrow(InvalidRequestException::new); + + var credentialMessage = protocolRegistry.forContext(DCP_SCOPE_V_1_0).transform(credentialMessageJson, CredentialMessage.class).orElseThrow(InvalidRequestException::new); + + var issuerScopes = selfIssuedTokenVerifier.verify(authtoken, participantContextId).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail()))); + + //todo: implement credential write + + return Response.ok().build(); + } + +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidator.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidator.java new file mode 100644 index 000000000..33527c5a8 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidator.java @@ -0,0 +1,75 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.validation; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +import static java.util.Optional.ofNullable; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage.CREDENTIALS_TERM; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage.REQUEST_ID_TERM; +import static org.eclipse.edc.validator.spi.ValidationResult.failure; +import static org.eclipse.edc.validator.spi.ValidationResult.success; +import static org.eclipse.edc.validator.spi.Violation.violation; + +/** + * Validates that a JsonObject representing a {@link org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage} contains + * a {@code requestId} and a {@code credentials} property + */ +public class CredentialMessageValidator implements Validator { + + private final JsonLdNamespace namespace = DSPACE_DCP_NAMESPACE_V_1_0; + + @Override + public ValidationResult validate(JsonObject input) { + if (input == null) { + return failure(violation("Credential message was null", ".")); + } + var requestId = input.get(namespace.toIri(REQUEST_ID_TERM)); + if (isNullObject(requestId)) { + return failure(violation("Must contain a 'requestId' property.", null)); + } + var credentialsObject = input.get(namespace.toIri(CREDENTIALS_TERM)); + if (isNullObject(credentialsObject)) { + return failure(violation("Credentials array was null", null)); + } + + return success(); + } + + /** + * Checks if the given JsonValue object is the Null-Object. Due to JSON-LD expansion, a {@code "key": null} + * would get expanded to an array, thus a simple equals-null check is not sufficient. + * + * @param value the JsonValue to check + * @return true if the JsonValue object is either null, or its value type is NULL, false otherwise + */ + private boolean isNullObject(JsonValue value) { + if (value instanceof JsonArray jarray) { + if (jarray.isEmpty()) { + return false; // empty arrays are OK + } + value = jarray.get(0).asJsonObject().get(JsonLdKeywords.VALUE); + return ofNullable(value).map(jv -> jv.getValueType() == JsonValue.ValueType.NULL).orElse(false); + } + return value == null; + } +} diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..b7c9a5123 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# 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 +# +# + +org.eclipse.edc.identityhub.api.StorageApiExtension \ No newline at end of file diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/storage-api-version.json b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/storage-api-version.json new file mode 100644 index 000000000..39b8b3b98 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/main/resources/storage-api-version.json @@ -0,0 +1,8 @@ +[ + { + "version": "0.0.1", + "urlPath": "/v1alpha", + "lastUpdated": "2025-02-10T12:00:00Z", + "maturity": null + } +] \ No newline at end of file diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/storage/StorageApiControllerTest.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/storage/StorageApiControllerTest.java new file mode 100644 index 000000000..a477940cf --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/storage/StorageApiControllerTest.java @@ -0,0 +1,199 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.storage; + +import com.nimbusds.jwt.JWTClaimsSet; +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialContainer; +import org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage; +import org.eclipse.edc.identityhub.spi.verification.SelfIssuedTokenVerifier; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.Instant; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0; +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt; +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.generateEcKey; +import static org.eclipse.edc.validator.spi.Violation.violation; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ApiTest +class StorageApiControllerTest extends RestControllerTestBase { + + private final JsonObjectValidatorRegistry validatorRegistry = mock(); + private final TypeTransformerRegistry transformerRegistry = mock(); + private final SelfIssuedTokenVerifier tokenVerifier = mock(SelfIssuedTokenVerifier.class); + private final Monitor monitor = mock(); + + @BeforeEach + void setUp() { + when(transformerRegistry.forContext(eq(DCP_SCOPE_V_1_0))).thenReturn(transformerRegistry); + when(transformerRegistry.transform(isA(JsonObject.class), eq(CredentialMessage.class))) + .thenReturn(Result.success(credentialMessage())); + } + + @Test + void storeCredential_success_expect200() { + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.success()); + when(tokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("foo-scope"))); + baseRequest() + .header("Authorization", "Bearer: " + generateJwt()) + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(200); + } + + @Test + void storeCredential_tokenNotPresent_shouldReturn401() { + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.success()); + baseRequest() + // missing: auth header + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(401); + verifyNoMoreInteractions(tokenVerifier, validatorRegistry, transformerRegistry); + } + + @Test + void storeCredential_validationError_shouldReturn400() { + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.failure(violation("foo", null))); + baseRequest() + .header("Authorization", "Bearer: " + generateJwt()) + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(400); + verifyNoInteractions(tokenVerifier, transformerRegistry); + } + + @Test + void storeCredential_transformationError_shouldReturn400() { + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.success()); + when(transformerRegistry.transform(isA(JsonObject.class), eq(CredentialMessage.class))).thenReturn(Result.failure("foobar")); + baseRequest() + .header("Authorization", "Bearer: " + generateJwt()) + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(400); + verifyNoMoreInteractions(tokenVerifier); + } + + @Test + void storeCredential_tokenValidationFails_shouldReturn401() { + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.success()); + when(tokenVerifier.verify(anyString(), anyString())).thenReturn(Result.failure("foo")); + baseRequest() + .header("Authorization", "Bearer: " + generateJwt()) + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(401); + } + + @Test + @Disabled + void storeCredential_whenWriteFails_shouldReturn500() { + // todo: add mock for credential storing service + when(validatorRegistry.validate(any(), any())).thenReturn(ValidationResult.success()); + when(tokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("foo-scope"))); + baseRequest() + .header("Authorization", "Bearer: " + generateJwt()) + .body(credentialMessageJson()) + .post() + .then() + .log().ifValidationFails() + .statusCode(500); + } + + @Override + protected Object controller() { + return new StorageApiController(validatorRegistry, + transformerRegistry, + tokenVerifier, + new TitaniumJsonLd(monitor)); + } + + private CredentialMessage credentialMessage() { + return CredentialMessage.Builder.newInstance() + .requestId(UUID.randomUUID().toString()) + .credential(new CredentialContainer("SomeCredential", "vcdm11_jwt", "SOME_JWT_STRING")) + .build(); + } + + private JsonObject credentialMessageJson() { + return Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), UUID.randomUUID().toString()) + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentialType"), "SomeCredential") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("format"), "vcdm11_jwt") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("payload"), "SOME_JWT_STRING"))) + .build(); + } + + private RequestSpecification baseRequest() { + var s = Base64.getUrlEncoder().encodeToString("test-participant".getBytes()); + + return given() + .contentType("application/json") + .baseUri("http://localhost:" + port + "/v1alpha/participants/" + s + "/credentials") + .when(); + } + + private String generateJwt() { + var ecKey = generateEcKey(null); + var jwt = buildSignedJwt(new JWTClaimsSet.Builder().audience("test-audience") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .issuer("test-issuer") + .subject("test-subject") + .jwtID(UUID.randomUUID().toString()).build(), ecKey); + + return jwt.serialize(); + } + +} \ No newline at end of file diff --git a/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidatorTest.java b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidatorTest.java new file mode 100644 index 000000000..8d52e1f18 --- /dev/null +++ b/extensions/protocols/dcp/dcp-identityhub/storage-api/src/test/java/org/eclipse/edc/identityhub/api/validation/CredentialMessageValidatorTest.java @@ -0,0 +1,99 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.api.validation; + +import jakarta.json.Json; +import jakarta.json.JsonValue; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.Mockito.mock; + +class CredentialMessageValidatorTest { + + private final CredentialMessageValidator validator = new CredentialMessageValidator(); + private final JsonLd jsonLd = new TitaniumJsonLd(mock()); + + @Test + void validate_success() { + var msg = Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), UUID.randomUUID().toString()) + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentialType"), "SomeCredential") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("format"), "vcdm11_jwt") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("payload"), "SOME_JWT_STRING"))) + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isSucceeded(); + } + + @Test + void validate_emptyCredentials_success() { + var msg = Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), UUID.randomUUID().toString()) + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), Json.createArrayBuilder()) + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isSucceeded(); + } + + @Test + void validate_requestIdMissing_failure() { + var msg = Json.createObjectBuilder() + // missing: requestId + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentialType"), "SomeCredential") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("format"), "vcdm11_jwt") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("payload"), "SOME_JWT_STRING"))) + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isFailed(); + } + + @Test + void validate_requestIdNull_failure() { + var msg = Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), JsonValue.NULL) + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentialType"), "SomeCredential") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("format"), "vcdm11_jwt") + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("payload"), "SOME_JWT_STRING"))) + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isFailed(); + } + + @Test + void validate_credentialsArrayNull_failure() { + var msg = Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), UUID.randomUUID().toString()) + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("credentials"), JsonValue.NULL) + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isFailed(); + } + + @Test + void validate_credentialsArrayMissing_failure() { + var msg = Json.createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri("requestId"), UUID.randomUUID().toString()) + // missing: credentials array + .build(); + assertThat(validator.validate(jsonLd.expand(msg).getContent())).isFailed(); + } +} diff --git a/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialContainer.java b/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialContainer.java new file mode 100644 index 000000000..d555840f4 --- /dev/null +++ b/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialContainer.java @@ -0,0 +1,19 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.protocols.dcp.spi.model; + +public record CredentialContainer(String credentialType, String format, String payload) { + +} diff --git a/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialMessage.java b/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialMessage.java new file mode 100644 index 000000000..cd776dd64 --- /dev/null +++ b/extensions/protocols/dcp/dcp-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/spi/model/CredentialMessage.java @@ -0,0 +1,68 @@ +/* + * 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 + * + */ + +package org.eclipse.edc.identityhub.protocols.dcp.spi.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; + +public class CredentialMessage { + public static final String CREDENTIALS_TERM = "credentials"; + public static final String REQUEST_ID_TERM = "requestId"; + public static final String CREDENTIAL_MESSAGE_TERM = "CredentialMessage"; + + private Collection credentials = new ArrayList<>(); + private String requestId; + + public Collection getCredentials() { + return credentials; + } + + public String getRequestId() { + return requestId; + } + + public static final class Builder { + private final CredentialMessage credentialMessage; + + private Builder(CredentialMessage credentialMessage) { + this.credentialMessage = credentialMessage; + } + + public static Builder newInstance() { + return new Builder(new CredentialMessage()); + } + + public Builder credentials(Collection credentials) { + this.credentialMessage.credentials = credentials; + return this; + } + + public Builder credential(CredentialContainer credential) { + this.credentialMessage.credentials.add(credential); + return this; + } + + public Builder requestId(String requestId) { + this.credentialMessage.requestId = requestId; + return this; + } + + public CredentialMessage build() { + Objects.requireNonNull(credentialMessage.requestId, "requestId"); + return credentialMessage; + } + } +} diff --git a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/attestationdefinition/schema/postgres/CredentialDefinitionMapping.java b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/attestationdefinition/schema/postgres/CredentialDefinitionMapping.java index 20b062641..b42bcb5d5 100644 --- a/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/attestationdefinition/schema/postgres/CredentialDefinitionMapping.java +++ b/extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/attestationdefinition/schema/postgres/CredentialDefinitionMapping.java @@ -50,7 +50,7 @@ public CredentialDefinitionMapping(CredentialDefinitionStoreStatements statement add(FIELD_JSON_SCHEMA_URL, statements.getJsonSchemaUrlColumn()); add(FIELD_VALIDITY, statements.getValidityColumn()); add(FIELD_DATAMODEL, statements.getDataModelColumn()); - add(FIELD_ATTESTATIONS, new JsonArrayTranslator()); + add(FIELD_ATTESTATIONS, new JsonArrayTranslator(statements.getAttestationsColumn())); add(FIELD_RULES, new JsonFieldTranslator(RULES_ALIAS)); add(FIELD_MAPPINGS, new JsonFieldTranslator(MAPPING_ALIAS)); } diff --git a/extensions/store/sql/issuerservice-participant-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/participant/schema/postgres/ParticipantMapping.java b/extensions/store/sql/issuerservice-participant-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/participant/schema/postgres/ParticipantMapping.java index 9c208d054..f40eebb61 100644 --- a/extensions/store/sql/issuerservice-participant-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/participant/schema/postgres/ParticipantMapping.java +++ b/extensions/store/sql/issuerservice-participant-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/participant/schema/postgres/ParticipantMapping.java @@ -37,6 +37,6 @@ public ParticipantMapping(ParticipantStoreStatements statements) { add(FIELD_LASTMODIFIED_TIMESTAMP, statements.getLastModifiedTimestampColumn()); add(FIELD_NAME, statements.getParticipantNameColumn()); add(FIELD_DID, statements.getDidColumn()); - add(FIELD_ATTESTATIONS, new JsonArrayTranslator()); + add(FIELD_ATTESTATIONS, new JsonArrayTranslator(statements.getAttestationsColumn())); } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 75e74c185..56da9313e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -75,7 +75,8 @@ include(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-transform-lib") include(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-api") include(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-core") -include(":extensions:protocols:dcp:presentation-api") +include(":extensions:protocols:dcp:dcp-identityhub:presentation-api") +include(":extensions:protocols:dcp:dcp-identityhub:storage-api") // Identity APIs diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/webcontext/IdentityHubApiContext.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/webcontext/IdentityHubApiContext.java index eede2e3a1..f05a460b5 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/webcontext/IdentityHubApiContext.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/webcontext/IdentityHubApiContext.java @@ -18,6 +18,7 @@ public interface IdentityHubApiContext { String IDENTITY = "identity"; String IH_DID = "did"; String PRESENTATION = "presentation"; + String STORAGE = "storage"; String ISSUANCE_API = "issuance"; @Deprecated(since = "0.9.0") String RESOLUTION = "resolution";