From 270e9a4d424ba6cd923474984e39a17156079d75 Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Mon, 10 Feb 2025 18:29:09 +0100 Subject: [PATCH] feat: first impl of DCP credential request API --- .github/workflows/verify.yaml | 1 + .../src/main/resources/dcp.v1.0.jsonld | 8 +- .../issuance/IssuanceServicesExtension.java | 43 ++- .../attestation/AttestationPipelineImpl.java | 2 +- ...CredentialRuleDefinitionEvaluatorImpl.java | 45 +++ .../CredentialRuleFactoryRegistryImpl.java | 2 +- .../AttestationPipelineImplTest.java | 10 +- ...CredentialRuleFactoryRegistryImplTest.java | 2 +- .../issuerservice-base-bom/build.gradle.kts | 4 + e2e-tests/admin-api-tests/build.gradle.kts | 1 + .../tests/dcp/DcpIssuanceApiEndToEndTest.java | 358 ++++++++++++++++++ .../IssuerServiceEndToEndExtension.java | 4 +- .../IssuerServiceEndToEndTestContext.java | 4 + .../IssuerServiceRuntimeConfiguration.java | 5 +- .../eclipse/edc/test/bom/BomSmokeTests.java | 1 + .../DefaultAttestationContext.java | 50 --- .../build.gradle.kts | 2 +- .../IssuanceAttestationsExtension.java | 38 ++ .../PresentationAttestationSource.java | 2 +- .../PresentationAttestationSourceFactory.java | 2 +- ...PresentationAttestatonSourceValidator.java | 2 +- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + ...sentationAttestationSourceFactoryTest.java | 7 +- .../PresentationAttestationSourceTest.java | 9 +- ...ntationAttestationSourceValidatorTest.java | 2 +- .../presentation/TestAttestationContext.java | 29 ++ .../build.gradle.kts | 34 ++ .../rules/IssuanceRulesExtension.java | 39 ++ .../issuance/rules}/JsonNavigator.java | 8 +- .../expression}/ExpressionCredentialRule.java | 4 +- ...sionCredentialRuleDefinitionValidator.java | 2 +- .../ExpressionCredentialRuleFactory.java | 4 +- ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../issuance/rules}/JsonNavigatorTest.java | 4 +- ...CredentialRuleDefinitionValidatorTest.java | 2 +- .../ExpressionCredentialRuleFactoryTest.java | 2 +- .../ExpressionCredentialRuleTest.java | 4 +- .../dcp-issuer-api/build.gradle.kts | 1 + .../dcp/issuer/IssuerApiExtension.java | 23 +- .../CredentialRequestApi.java | 2 +- .../CredentialRequestApiController.java | 55 ++- .../CredentialRequestMessageValidator.java | 35 ++ .../main/resources/issuer-api-version.json | 2 +- .../CredentialRequestApiControllerTest.java | 189 +++++++++ ...CredentialRequestMessageValidatorTest.java | 48 +++ .../dcp-issuer-core/build.gradle.kts | 34 ++ .../dcp/issuer/DcpAttestationContext.java | 36 ++ .../dcp/issuer/DcpIssuerCoreExtension.java | 106 ++++++ .../DcpIssuerSelfIssuedTokenVerifierImpl.java | 81 ++++ .../dcp/issuer/DcpIssuerServiceImpl.java | 143 +++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../issuer/DcpIssuerCoreExtensionTest.java | 71 ++++ ...IssuerSelfIssuedTokenVerifierImplTest.java | 108 ++++++ .../dcp/issuer/DcpIssuerServiceImplTest.java | 103 +++++ .../dcp-issuer-spi/build.gradle.kts | 1 + .../spi/DcpIssuerSelfIssuedTokenVerifier.java | 31 ++ .../dcp/issuer/spi/DcpIssuerService.java | 34 ++ .../spi/model/CredentialRequestMessage.java | 7 + .../issuer/spi/model/DcpRequestContext.java | 28 ++ gradle/libs.versions.toml | 1 + settings.gradle.kts | 4 +- .../CredentialRuleDefinitionEvaluator.java | 38 ++ 62 files changed, 1857 insertions(+), 105 deletions(-) rename {extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials => core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance}/attestation/AttestationPipelineImpl.java (97%) create mode 100644 core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleDefinitionEvaluatorImpl.java rename {extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials => core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance}/rule/CredentialRuleFactoryRegistryImpl.java (94%) rename {extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials => core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance}/attestation/AttestationPipelineImplTest.java (92%) rename {extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials => core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance}/rule/CredentialRuleFactoryRegistryImplTest.java (93%) create mode 100644 e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/DcpIssuanceApiEndToEndTest.java delete mode 100644 extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/DefaultAttestationContext.java rename extensions/issuance/{issuance-credentials => issuerservice-issuance-attestations}/build.gradle.kts (89%) create mode 100644 extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/IssuanceAttestationsExtension.java rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestationSource.java (95%) rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestationSourceFactory.java (95%) rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestatonSourceValidator.java (95%) create mode 100644 extensions/issuance/issuerservice-issuance-attestations/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestationSourceFactoryTest.java (78%) rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestationSourceTest.java (69%) rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation => issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations}/presentation/PresentationAttestationSourceValidatorTest.java (95%) create mode 100644 extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/TestAttestationContext.java create mode 100644 extensions/issuance/issuerservice-issuance-rules/build.gradle.kts create mode 100644 extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/IssuanceRulesExtension.java rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/json => issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules}/JsonNavigator.java (95%) rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRule.java (96%) rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRuleDefinitionValidator.java (97%) rename extensions/issuance/{issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRuleFactory.java (86%) create mode 100644 extensions/issuance/issuerservice-issuance-rules/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/json => issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules}/JsonNavigatorTest.java (92%) rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRuleDefinitionValidatorTest.java (97%) rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRuleFactoryTest.java (94%) rename extensions/issuance/{issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule => issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression}/ExpressionCredentialRuleTest.java (93%) create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidator.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiControllerTest.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidatorTest.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/build.gradle.kts create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpAttestationContext.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImpl.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtensionTest.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImplTest.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerSelfIssuedTokenVerifier.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerService.java create mode 100644 extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/DcpRequestContext.java create mode 100644 spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/rule/CredentialRuleDefinitionEvaluator.java diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 48dfcd2b9..6ffd6721f 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -80,6 +80,7 @@ jobs: -e "EDC_STS_ACCOUNT_API_URL=https://sts.com" \ -e "EDC_STS_ACCOUNTS_API_AUTH_HEADER_VALUE=auth-header" \ -e "EDC_ISSUER_STATUSLIST_SIGNING_KEY_ALIAS=foo-alias" \ + -e "EDC_ISSUER_ID=did:web:issuer" \ issuer-service:latest - name: 'Wait for Issuer-Service to be healthy' diff --git a/core/identity-hub-core/src/main/resources/dcp.v1.0.jsonld b/core/identity-hub-core/src/main/resources/dcp.v1.0.jsonld index f7858e342..fb916cbe7 100644 --- a/core/identity-hub-core/src/main/resources/dcp.v1.0.jsonld +++ b/core/identity-hub-core/src/main/resources/dcp.v1.0.jsonld @@ -69,11 +69,9 @@ "CredentialRequestMessage": { "@id": "dcp:CredentialRequestMessage", "@context": { - "format": "dcp:format", - "credentialType": { - "@id": "dcp:credentialType", - "@type": "@vocab", - "@container": "@set" + "credentials": { + "@id": "dcp:credentials", + "@type": "@json" } } }, diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceServicesExtension.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceServicesExtension.java index 436692cff..e34ee3213 100644 --- a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceServicesExtension.java +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceServicesExtension.java @@ -15,11 +15,18 @@ package org.eclipse.edc.issuerservice.issuance; import org.eclipse.edc.issuerservice.issuance.attestation.AttestationDefinitionServiceImpl; +import org.eclipse.edc.issuerservice.issuance.attestation.AttestationPipelineImpl; import org.eclipse.edc.issuerservice.issuance.credentialdefinition.CredentialDefinitionServiceImpl; +import org.eclipse.edc.issuerservice.issuance.rule.CredentialRuleDefinitionEvaluatorImpl; +import org.eclipse.edc.issuerservice.issuance.rule.CredentialRuleFactoryRegistryImpl; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionService; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionStore; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationPipeline; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactoryRegistry; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactoryRegistry; import org.eclipse.edc.issuerservice.spi.participant.store.ParticipantStore; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -36,11 +43,14 @@ public class IssuanceServicesExtension implements ServiceExtension { private TransactionContext transactionContext; @Inject private CredentialDefinitionStore store; - @Inject private AttestationDefinitionStore attestationDefinitionStore; @Inject private ParticipantStore participantStore; + + private AttestationPipelineImpl attestationPipeline; + + private CredentialRuleFactoryRegistry ruleFactoryRegistry; @Provider public CredentialDefinitionService createParticipantService() { @@ -51,4 +61,35 @@ public CredentialDefinitionService createParticipantService() { public AttestationDefinitionService createAttestationService() { return new AttestationDefinitionServiceImpl(transactionContext, attestationDefinitionStore, participantStore); } + + @Provider + public AttestationPipeline createAttestationPipeline() { + return createAttestationPipelineImpl(); + } + + @Provider + public AttestationSourceFactoryRegistry createAttestationSourceFactoryRegistry() { + return createAttestationPipelineImpl(); + } + + @Provider + public CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator() { + return new CredentialRuleDefinitionEvaluatorImpl(credentialRuleFactoryRegistry()); + } + + @Provider + public CredentialRuleFactoryRegistry credentialRuleFactoryRegistry() { + + if (ruleFactoryRegistry == null) { + ruleFactoryRegistry = new CredentialRuleFactoryRegistryImpl(); + } + return ruleFactoryRegistry; + } + + private AttestationPipelineImpl createAttestationPipelineImpl() { + if (attestationPipeline == null) { + attestationPipeline = new AttestationPipelineImpl(attestationDefinitionStore); + } + return attestationPipeline; + } } diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImpl.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImpl.java similarity index 97% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImpl.java rename to core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImpl.java index e540808ea..8b965932b 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImpl.java +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImpl.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation; +package org.eclipse.edc.issuerservice.issuance.attestation; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionStore; diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleDefinitionEvaluatorImpl.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleDefinitionEvaluatorImpl.java new file mode 100644 index 000000000..76a363724 --- /dev/null +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleDefinitionEvaluatorImpl.java @@ -0,0 +1,45 @@ +/* + * 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.issuerservice.issuance.rule; + +import org.eclipse.edc.issuerservice.spi.issuance.IssuanceContext; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactoryRegistry; +import org.eclipse.edc.spi.result.Result; + +import java.util.Collection; + +public class CredentialRuleDefinitionEvaluatorImpl implements CredentialRuleDefinitionEvaluator { + + private final CredentialRuleFactoryRegistry credentialRuleFactoryRegistry; + + public CredentialRuleDefinitionEvaluatorImpl(CredentialRuleFactoryRegistry credentialRuleFactoryRegistry) { + this.credentialRuleFactoryRegistry = credentialRuleFactoryRegistry; + } + + @Override + public Result evaluate(Collection definitions, IssuanceContext context) { + for (var definition : definitions) { + var factory = credentialRuleFactoryRegistry.resolveFactory(definition.type()); + var rule = factory.createRule(definition); + var result = rule.evaluate(context); + if (result.failed()) { + return result; + } + } + return Result.success(); + } +} diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImpl.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImpl.java similarity index 94% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImpl.java rename to core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImpl.java index 46864d546..76e8bb857 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImpl.java +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImpl.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rule; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactory; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactoryRegistry; diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImplTest.java b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImplTest.java similarity index 92% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImplTest.java rename to core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImplTest.java index 963d8563a..e2f58c21e 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/AttestationPipelineImplTest.java +++ b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/attestation/AttestationPipelineImplTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation; +package org.eclipse.edc.issuerservice.issuance.attestation; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionStore; @@ -26,7 +26,6 @@ import java.util.Map; import java.util.Set; -import static java.util.Collections.emptyMap; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; @@ -46,6 +45,8 @@ void evaluate_whenSingle_success() { var attestationDefinition = new AttestationDefinition("a123", "testType", Map.of()); var store = mock(AttestationDefinitionStore.class); + var attestationContext = mock(AttestationContext.class); + when(store.resolveDefinition(eq("a123"))).thenReturn(attestationDefinition); var pipeline = new AttestationPipelineImpl(store); @@ -58,7 +59,7 @@ void evaluate_whenSingle_success() { pipeline.registerFactory("testType", sourceFactory); - var results = pipeline.evaluate(Set.of("a123"), new DefaultAttestationContext("123", emptyMap())); + var results = pipeline.evaluate(Set.of("a123"), attestationContext); assertThat(results).isSucceeded(); assertThat(results.getContent()).contains(entry("test", "value")); @@ -75,6 +76,7 @@ void evaluate_whenMultipleInvalid_shouldFailOnFirst() { var attestationDefinition2 = new AttestationDefinition("a456", "testType1", Map.of()); var store = mock(AttestationDefinitionStore.class); + var attestationContext = mock(AttestationContext.class); when(store.resolveDefinition(eq("a123"))).thenReturn(attestationDefinition1); when(store.resolveDefinition(eq("a456"))).thenReturn(attestationDefinition2); @@ -88,7 +90,7 @@ void evaluate_whenMultipleInvalid_shouldFailOnFirst() { pipeline.registerFactory("testType1", sourceFactory); - var results = pipeline.evaluate(new LinkedHashSet<>(List.of("a123", "a456")), new DefaultAttestationContext("123", emptyMap())); + var results = pipeline.evaluate(new LinkedHashSet<>(List.of("a123", "a456")), attestationContext); assertThat(results).isFailed(); verify(store).resolveDefinition("a123"); diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImplTest.java b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImplTest.java similarity index 93% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImplTest.java rename to core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImplTest.java index febe07b93..53371dccf 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/CredentialRuleFactoryRegistryImplTest.java +++ b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/rule/CredentialRuleFactoryRegistryImplTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rule; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactory; import org.junit.jupiter.api.Test; diff --git a/dist/bom/issuerservice-base-bom/build.gradle.kts b/dist/bom/issuerservice-base-bom/build.gradle.kts index c3a3ef6ba..c9a2d421b 100644 --- a/dist/bom/issuerservice-base-bom/build.gradle.kts +++ b/dist/bom/issuerservice-base-bom/build.gradle.kts @@ -28,8 +28,12 @@ dependencies { runtimeOnly(project(":core:issuerservice:issuerservice-issuance")) runtimeOnly(project(":extensions:did:local-did-publisher")) // API modules + runtimeOnly(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-core")) runtimeOnly(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-api")) + runtimeOnly(project(":extensions:issuance:issuerservice-issuance-attestations")) + runtimeOnly(project(":extensions:issuance:issuerservice-issuance-rules")) + runtimeOnly(project(":extensions:sts:sts-account-provisioner")) runtimeOnly(libs.edc.identity.did.core) runtimeOnly(libs.edc.core.token) diff --git a/e2e-tests/admin-api-tests/build.gradle.kts b/e2e-tests/admin-api-tests/build.gradle.kts index 70fd9db66..bbf9a1446 100644 --- a/e2e-tests/admin-api-tests/build.gradle.kts +++ b/e2e-tests/admin-api-tests/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { testImplementation(libs.jakarta.rsApi) testImplementation(libs.edc.sts.spi) testImplementation(testFixtures(project(":e2e-tests:fixtures"))) + testImplementation(testFixtures(project(":spi:verifiable-credential-spi"))) } diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/DcpIssuanceApiEndToEndTest.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/DcpIssuanceApiEndToEndTest.java new file mode 100644 index 000000000..01a4a2a34 --- /dev/null +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/DcpIssuanceApiEndToEndTest.java @@ -0,0 +1,358 @@ +/* + * 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.tests.dcp; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; +import org.eclipse.edc.identityhub.tests.fixtures.IssuerServiceEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IssuerServiceEndToEndTestContext; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionStore; +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.attestation.AttestationSourceFactoryRegistry; +import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; +import org.eclipse.edc.issuerservice.spi.participant.ParticipantService; +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.Result; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Map; + +import static io.restassured.http.ContentType.JSON; +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil.generateJwt; +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.generateEcKey; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DcpIssuanceApiEndToEndTest { + + protected static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock(); + + + abstract static class Tests { + + public static final String ISSUER_DID = "did:web:issuer"; + public static final String PARTICIPANT_DID = "did:web:participant"; + public static final String DID_WEB_PARTICIPANT_KEY_1 = "did:web:participant#key1"; + public static final ECKey PARTICIPANT_KEY = generateEcKey(DID_WEB_PARTICIPANT_KEY_1); + protected static final AttestationSourceFactory ATTESTATION_SOURCE_FACTORY = mock(); + private static final String VALID_CREDENTIAL_REQUEST_MESSAGE = """ + { + "@context": [ + "https://w3id.org/dspace-dcp/v1.0/dcp.jsonld" + ], + "@type": "CredentialRequestMessage", + "credentials":[ + { + "credentialType": "MembershipCredential", + "format": "vcdm11_jwt" + } + ] + } + """; + private static final String FAULTY_CREDENTIAL_REQUEST_MESSAGE = """ + { + "@context": [ + "https://w3id.org/dspace-dcp/v1.0/dcp.jsonld" + ], + "@type": "CredentialRequestMessage" + } + """; + + @BeforeAll + static void beforeAll(IssuerServiceEndToEndTestContext context) { + var pipelineFactory = context.getRuntime().getService(AttestationSourceFactoryRegistry.class); + pipelineFactory.registerFactory("Attestation", ATTESTATION_SOURCE_FACTORY); + } + + @AfterEach + void teardown(ParticipantService participantService, CredentialDefinitionService credentialDefinitionService) { + participantService.queryParticipants(QuerySpec.max()).getContent() + .forEach(p -> participantService.deleteParticipant(p.participantId()).getContent()); + + credentialDefinitionService.queryCredentialDefinitions(QuerySpec.max()).getContent() + .forEach(c -> credentialDefinitionService.deleteCredentialDefinition(c.getId()).getContent()); + } + + @Test + void requestCredential(IssuerServiceEndToEndTestContext context, ParticipantService participantService, + CredentialDefinitionService credentialDefinitionService, + AttestationDefinitionStore attestationDefinitionStore, + IssuanceProcessStore issuanceProcessStore) throws JOSEException { + + participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant")); + + var attestationDefinition = new AttestationDefinition("attestation-id", "Attestation", Map.of()); + attestationDefinitionStore.create(attestationDefinition); + + Map credentialRuleConfiguration = Map.of( + "claim", "onboarding.signedDocuments", + "operator", "eq", + "value", true); + + var credentialDefinition = CredentialDefinition.Builder.newInstance() + .id("credential-id") + .credentialType("MembershipCredential") + .jsonSchemaUrl("https://example.com/schema") + .jsonSchema("{}") + .attestation("attestation-id") + .rule(new CredentialRuleDefinition("expression", credentialRuleConfiguration)) + .build(); + + credentialDefinitionService.createCredentialDefinition(credentialDefinition); + + var token = generateSiToken(); + + Map claims = Map.of("onboarding", Map.of("signedDocuments", true)); + + var attestationSource = mock(AttestationSource.class); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey())); + when(ATTESTATION_SOURCE_FACTORY.createSource(eq(attestationDefinition))).thenReturn(attestationSource); + when(attestationSource.execute(any())).thenReturn(Result.success(claims)); + + var location = context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(201) + .extract() + .header("Location"); + + assertThat(location).contains("/v1alpha/requests/"); + + var processId = location.substring(location.lastIndexOf('/') + 1); + var issuanceProcess = issuanceProcessStore.findById(processId); + + assertThat(issuanceProcess).isNotNull() + .satisfies(process -> { + assertThat(process.getParticipantId()).isEqualTo(PARTICIPANT_DID); + assertThat(process.getCredentialDefinitions()).containsExactly("credential-id"); + assertThat(process.getClaims()).containsAllEntriesOf(claims); + }); + + } + + @Test + void requestCredential_validationError_shouldReturn400(IssuerServiceEndToEndTestContext context) { + var token = generateSiToken(); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(FAULTY_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(400); + + } + + @Test + void requestCredential_tokenNotPresent_shouldReturn401(IssuerServiceEndToEndTestContext context) { + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(401); + + } + + @Test + void requestCredential_participantNotFound_shouldReturn401(IssuerServiceEndToEndTestContext context) { + var token = generateSiToken(); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(401); + + } + + @Test + void requestCredential_tokenVerificationFails_shouldReturn401(IssuerServiceEndToEndTestContext context, ParticipantService participantService) throws JOSEException { + + participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant")); + + var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID(DID_WEB_PARTICIPANT_KEY_1).generate(); + + var token = generateSiToken(); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(spoofedKey.toPublicKey())); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(401); + + } + + @Test + void requestCredential_wrongTokenAudience_shouldReturn401(IssuerServiceEndToEndTestContext context, ParticipantService participantService) throws JOSEException { + + participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant")); + + var token = generateSiToken("wrong-audience"); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey())); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(401); + + } + + @Test + void requestCredential_definitionNotFound_shouldReturn400(IssuerServiceEndToEndTestContext context, ParticipantService participantService) throws JOSEException { + + participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant")); + + var token = generateSiToken(); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey())); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(400); + + } + + @Test + void requestCredential_attestationsNotFulfilled_shouldReturn403(IssuerServiceEndToEndTestContext context, + ParticipantService participantService, + AttestationDefinitionStore attestationDefinitionStore, + CredentialDefinitionService credentialDefinitionService) throws JOSEException { + + participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant")); + var attestationDefinition = new AttestationDefinition("attestation-id", "Attestation", Map.of()); + attestationDefinitionStore.create(attestationDefinition); + + Map credentialRuleConfiguration = Map.of( + "claim", "onboarding.signedDocuments", + "operator", "eq", + "value", true); + + + var credentialDefinition = CredentialDefinition.Builder.newInstance() + .id("credential-id") + .credentialType("MembershipCredential") + .jsonSchemaUrl("https://example.com/schema") + .jsonSchema("{}") + .attestation("attestation-id") + .rule(new CredentialRuleDefinition("expression", credentialRuleConfiguration)) + .build(); + + credentialDefinitionService.createCredentialDefinition(credentialDefinition); + var token = generateSiToken(); + + Map claims = Map.of("onboarding", Map.of("signedDocuments", false)); + + var attestationSource = mock(AttestationSource.class); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey())); + when(ATTESTATION_SOURCE_FACTORY.createSource(eq(attestationDefinition))).thenReturn(attestationSource); + when(attestationSource.execute(any())).thenReturn(Result.success(claims)); + + context.getDcpIssuanceEndpoint().baseRequest() + .contentType(JSON) + .header(AUTHORIZATION, token) + .body(VALID_CREDENTIAL_REQUEST_MESSAGE) + .post("/v1alpha/credentials") + .then() + .log().ifValidationFails() + .statusCode(403); + + } + + private String generateSiToken() { + return generateSiToken(ISSUER_DID); + } + + private String generateSiToken(String audience) { + return generateJwt(audience, PARTICIPANT_DID, PARTICIPANT_DID, Map.of(), PARTICIPANT_KEY); + } + } + + + @Nested + @EndToEndTest + @Order(1) + class InMemory extends Tests { + + + @RegisterExtension + static IssuerServiceEndToEndExtension runtime; + + static { + runtime = new IssuerServiceEndToEndExtension.InMemory(); + runtime.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); + } + + } + + @Nested + @PostgresqlIntegrationTest + class Postgres extends Tests { + + @RegisterExtension + static IssuerServiceEndToEndExtension runtime; + + static { + runtime = new IssuerServiceEndToEndExtension.Postgres(); + runtime.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); + } + } +} diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndExtension.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndExtension.java index 4b2a34b0a..fb433d8b9 100644 --- a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndExtension.java +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndExtension.java @@ -63,7 +63,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte */ public static class InMemory extends IssuerServiceEndToEndExtension { - protected InMemory() { + public InMemory() { super(context()); } @@ -93,7 +93,7 @@ public static class Postgres extends IssuerServiceEndToEndExtension { private static final Integer DB_PORT = getFreePort(); private final PostgresSqlService postgresSqlService; - protected Postgres() { + public Postgres() { super(context(DB_NAME, DB_PORT)); postgresSqlService = new PostgresSqlService(DB_NAME, DB_PORT); diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndTestContext.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndTestContext.java index 768117904..abac072bf 100644 --- a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndTestContext.java +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceEndToEndTestContext.java @@ -38,4 +38,8 @@ public EmbeddedRuntime getRuntime() { public IssuerServiceRuntimeConfiguration.Endpoint getAdminEndpoint() { return configuration.getAdminEndpoint(); } + + public IssuerServiceRuntimeConfiguration.Endpoint getDcpIssuanceEndpoint() { + return configuration.getIssuerApiEndpoint(); + } } diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceRuntimeConfiguration.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceRuntimeConfiguration.java index 5e14d866b..cf0dd638e 100644 --- a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceRuntimeConfiguration.java +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IssuerServiceRuntimeConfiguration.java @@ -51,8 +51,8 @@ public Map config() { put("web.http.issueradmin.port", String.valueOf(adminEndpoint.getUrl().getPort())); put("web.http.issueradmin.path", adminEndpoint.getUrl().getPath()); - put("web.http.issuer-api.port", String.valueOf(issuerApiEndpoint.getUrl().getPort())); - put("web.http.issuer-api.path", issuerApiEndpoint.getUrl().getPath()); + put("web.http.issuance.port", String.valueOf(issuerApiEndpoint.getUrl().getPort())); + put("web.http.issuance.path", issuerApiEndpoint.getUrl().getPath()); put("web.http.version.port", String.valueOf(getFreePort())); put("web.http.version.path", "/.well-known/api"); put("web.http.did.port", String.valueOf(getFreePort())); @@ -63,6 +63,7 @@ public Map config() { put("edc.sts.accounts.api.auth.header.value", "password"); put("edc.iam.accesstoken.jti.validation", String.valueOf(false)); put("edc.issuer.statuslist.signing.key.alias", "signing-key"); + put("edc.issuer.id", "did:web:issuer"); } }; } 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 ebd5783bf..a95448064 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 @@ -129,6 +129,7 @@ class IssuerService extends SmokeTest { put("edc.sts.account.api.url", "https://sts.com/accounts"); put("edc.sts.accounts.api.auth.header.value", "password"); put("edc.issuer.statuslist.signing.key.alias", "signing-key"); + put("edc.issuer.id", "did:web:issuer"); } }, ":dist:bom:issuerservice-bom" diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/DefaultAttestationContext.java b/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/DefaultAttestationContext.java deleted file mode 100644 index 83903ca9f..000000000 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/DefaultAttestationContext.java +++ /dev/null @@ -1,50 +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 - * - */ - -package org.eclipse.edc.identityhub.issuance.credentials.attestation; - -import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; -import org.eclipse.edc.spi.iam.ClaimToken; -import org.jetbrains.annotations.Nullable; - -import java.util.Map; - -import static java.util.Objects.requireNonNull; - -/** - * Default context. - */ -public class DefaultAttestationContext implements AttestationContext { - private final Map claims; - private final String participantId; - - public DefaultAttestationContext(String participantId, Map claims) { - this.participantId = requireNonNull(participantId, "participantId"); - this.claims = requireNonNull(claims, "claims"); - } - - @Override - public @Nullable ClaimToken getClaimToken(String type) { - return claims.get(type); - } - - @Override - public String participantId() { - return participantId; - } - - public Map getClaims() { - return claims; - } -} diff --git a/extensions/issuance/issuance-credentials/build.gradle.kts b/extensions/issuance/issuerservice-issuance-attestations/build.gradle.kts similarity index 89% rename from extensions/issuance/issuance-credentials/build.gradle.kts rename to extensions/issuance/issuerservice-issuance-attestations/build.gradle.kts index 42ba51cd1..186270009 100644 --- a/extensions/issuance/issuance-credentials/build.gradle.kts +++ b/extensions/issuance/issuerservice-issuance-attestations/build.gradle.kts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * Cofinity-X - initial API and implementation * */ diff --git a/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/IssuanceAttestationsExtension.java b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/IssuanceAttestationsExtension.java new file mode 100644 index 000000000..d3c7d1f71 --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/IssuanceAttestationsExtension.java @@ -0,0 +1,38 @@ +/* + * 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.issuerservice.issuance.attestations; + +import org.eclipse.edc.issuerservice.issuance.attestations.presentation.PresentationAttestationSourceFactory; +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.issuance.attestations.IssuanceAttestationsExtension.NAME; + +@Extension(NAME) +public class IssuanceAttestationsExtension implements ServiceExtension { + + public static final String NAME = "Issuance Attestations Extension"; + + @Inject + private AttestationSourceFactoryRegistry registry; + + @Override + public void initialize(ServiceExtensionContext context) { + registry.registerFactory("presentation", new PresentationAttestationSourceFactory()); + } +} diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSource.java b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSource.java similarity index 95% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSource.java rename to extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSource.java index efd692a8d..70460929a 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSource.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSource.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSource; diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactory.java b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactory.java similarity index 95% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactory.java rename to extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactory.java index 11f586210..500321481 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactory.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactory.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSource; diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestatonSourceValidator.java b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestatonSourceValidator.java similarity index 95% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestatonSourceValidator.java rename to extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestatonSourceValidator.java index 5e629a7ca..fa365e419 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestatonSourceValidator.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/main/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestatonSourceValidator.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; diff --git a/extensions/issuance/issuerservice-issuance-attestations/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/issuance/issuerservice-issuance-attestations/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..195073c3f --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-attestations/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.issuerservice.issuance.attestations.IssuanceAttestationsExtension \ No newline at end of file diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactoryTest.java b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactoryTest.java similarity index 78% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactoryTest.java rename to extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactoryTest.java index 90c322698..835199568 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceFactoryTest.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceFactoryTest.java @@ -12,9 +12,8 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; -import org.eclipse.edc.identityhub.issuance.credentials.attestation.DefaultAttestationContext; import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; import org.eclipse.edc.spi.iam.ClaimToken; import org.junit.jupiter.api.Test; @@ -35,7 +34,7 @@ void verify_create() { var claims = Map.of("testCred", ClaimToken.Builder.newInstance().build()); - assertThat(source.execute(new DefaultAttestationContext("participant", claims)).succeeded()).isTrue(); + assertThat(source.execute(new TestAttestationContext("participant", claims)).succeeded()).isTrue(); } @Test @@ -46,7 +45,7 @@ void verify_create_optional() { var source = factory.createSource(definition); - assertThat(source.execute(new DefaultAttestationContext("participant", Map.of())).succeeded()).isTrue(); + assertThat(source.execute(new TestAttestationContext("participant", Map.of())).succeeded()).isTrue(); } diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceTest.java b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceTest.java similarity index 69% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceTest.java rename to extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceTest.java index bdbb8ee21..cd72217d8 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceTest.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceTest.java @@ -12,9 +12,8 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; -import org.eclipse.edc.identityhub.issuance.credentials.attestation.DefaultAttestationContext; import org.eclipse.edc.spi.iam.ClaimToken; import org.junit.jupiter.api.Test; @@ -28,7 +27,7 @@ class PresentationAttestationSourceTest { void verify_required() { var source = new PresentationAttestationSource("test", "testOut", true); var claims = Map.of("test", ClaimToken.Builder.newInstance().build()); - var result = source.execute(new DefaultAttestationContext("participant", claims)); + var result = source.execute(new TestAttestationContext("participant", claims)); assertThat(result.succeeded()).isTrue(); assertThat(result.getContent()).containsKey("testOut"); } @@ -36,12 +35,12 @@ void verify_required() { @Test void verify_optional() { var source = new PresentationAttestationSource("test", "testOut", false); - assertThat(source.execute(new DefaultAttestationContext("participant", Map.of())).succeeded()).isTrue(); + assertThat(source.execute(new TestAttestationContext("participant", Map.of())).succeeded()).isTrue(); } @Test void verify_notPresent() { var source = new PresentationAttestationSource("test", "testOut", true); - assertThat(source.execute(new DefaultAttestationContext("participant", Map.of())).failed()).isTrue(); + assertThat(source.execute(new TestAttestationContext("participant", Map.of())).failed()).isTrue(); } } \ No newline at end of file diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceValidatorTest.java b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceValidatorTest.java similarity index 95% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceValidatorTest.java rename to extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceValidatorTest.java index 5cd9065cb..2a83b45c5 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/attestation/presentation/PresentationAttestationSourceValidatorTest.java +++ b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/PresentationAttestationSourceValidatorTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.attestation.presentation; +package org.eclipse.edc.issuerservice.issuance.attestations.presentation; import org.eclipse.edc.issuerservice.spi.issuance.model.AttestationDefinition; import org.junit.jupiter.api.Test; diff --git a/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/TestAttestationContext.java b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/TestAttestationContext.java new file mode 100644 index 000000000..cb3a48854 --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-attestations/src/test/java/org/eclipse/edc/issuerservice/issuance/attestations/presentation/TestAttestationContext.java @@ -0,0 +1,29 @@ +/* + * 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.issuerservice.issuance.attestations.presentation; + +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public record TestAttestationContext(String participantId, + Map claims) implements AttestationContext { + @Override + public @Nullable ClaimToken getClaimToken(String type) { + return claims.get(type); + } +} diff --git a/extensions/issuance/issuerservice-issuance-rules/build.gradle.kts b/extensions/issuance/issuerservice-issuance-rules/build.gradle.kts new file mode 100644 index 000000000..186270009 --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-rules/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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` + `java-test-fixtures` + `maven-publish` +} + +dependencies { + api(project(":spi:issuerservice:issuerservice-issuance-spi")) + api(libs.edc.spi.core) + + implementation(libs.edc.spi.validator) + + testImplementation(libs.edc.junit) + testImplementation(libs.edc.lib.json) + + testFixturesImplementation(libs.edc.spi.identity.did) + testFixturesImplementation(libs.junit.jupiter.api) + testFixturesImplementation(libs.edc.junit) + testFixturesImplementation(libs.assertj) +} diff --git a/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/IssuanceRulesExtension.java b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/IssuanceRulesExtension.java new file mode 100644 index 000000000..2e36b78b6 --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/IssuanceRulesExtension.java @@ -0,0 +1,39 @@ +/* + * 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.issuerservice.issuance.rules; + +import org.eclipse.edc.issuerservice.issuance.rules.expression.ExpressionCredentialRuleFactory; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactoryRegistry; +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.issuance.rules.IssuanceRulesExtension.NAME; + + +@Extension(NAME) +public class IssuanceRulesExtension implements ServiceExtension { + + public static final String NAME = "Issuance Rules Extension"; + + @Inject + private CredentialRuleFactoryRegistry registry; + + @Override + public void initialize(ServiceExtensionContext context) { + registry.registerFactory("expression", new ExpressionCredentialRuleFactory()); + } +} diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigator.java b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigator.java similarity index 95% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigator.java rename to extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigator.java index 6daf763d7..e640f06c3 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigator.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigator.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.json; +package org.eclipse.edc.issuerservice.issuance.rules; import org.eclipse.edc.spi.result.Result; @@ -28,6 +28,9 @@ */ public class JsonNavigator { + private JsonNavigator() { + } + public static Result navigateProperty(String[] path, Map input, boolean required) { Object result = input; for (var property : path) { @@ -42,7 +45,4 @@ public static Result navigateProperty(String[] path, Map } return result == null && required ? failure(format("Property not found for path %s", join(".", path))) : success(result); } - - private JsonNavigator() { - } } diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRule.java b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRule.java similarity index 96% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRule.java rename to extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRule.java index ba71cd46d..330fe83d1 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRule.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRule.java @@ -12,13 +12,13 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; import org.eclipse.edc.issuerservice.spi.issuance.IssuanceContext; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRule; import org.eclipse.edc.spi.result.Result; -import static org.eclipse.edc.identityhub.issuance.credentials.json.JsonNavigator.navigateProperty; +import static org.eclipse.edc.issuerservice.issuance.rules.JsonNavigator.navigateProperty; import static org.eclipse.edc.spi.result.Result.success; /** diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidator.java b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidator.java similarity index 97% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidator.java rename to extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidator.java index d54439457..36535ce88 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidator.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidator.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; import org.eclipse.edc.validator.spi.ValidationResult; diff --git a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactory.java b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactory.java similarity index 86% rename from extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactory.java rename to extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactory.java index e0838e81c..9441abd07 100644 --- a/extensions/issuance/issuance-credentials/src/main/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactory.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/main/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactory.java @@ -12,9 +12,9 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; -import org.eclipse.edc.identityhub.issuance.credentials.rule.ExpressionCredentialRule.Operator; +import org.eclipse.edc.issuerservice.issuance.rules.expression.ExpressionCredentialRule.Operator; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactory; diff --git a/extensions/issuance/issuerservice-issuance-rules/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/issuance/issuerservice-issuance-rules/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..1bce0e446 --- /dev/null +++ b/extensions/issuance/issuerservice-issuance-rules/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.issuerservice.issuance.rules.IssuanceRulesExtension \ No newline at end of file diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigatorTest.java b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigatorTest.java similarity index 92% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigatorTest.java rename to extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigatorTest.java index 577739a39..bb3ac2fc8 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/json/JsonNavigatorTest.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/JsonNavigatorTest.java @@ -12,14 +12,14 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.json; +package org.eclipse.edc.issuerservice.issuance.rules; import org.junit.jupiter.api.Test; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.identityhub.issuance.credentials.json.JsonNavigator.navigateProperty; +import static org.eclipse.edc.issuerservice.issuance.rules.JsonNavigator.navigateProperty; class JsonNavigatorTest { diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidatorTest.java b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidatorTest.java similarity index 97% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidatorTest.java rename to extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidatorTest.java index 84e5ea305..9fa5970fb 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleDefinitionValidatorTest.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleDefinitionValidatorTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; import org.junit.jupiter.api.BeforeEach; diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactoryTest.java b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactoryTest.java similarity index 94% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactoryTest.java rename to extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactoryTest.java index 4aaf9d225..092d09b2d 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleFactoryTest.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleFactoryTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; import org.junit.jupiter.api.BeforeEach; diff --git a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleTest.java b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleTest.java similarity index 93% rename from extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleTest.java rename to extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleTest.java index 90608bcc7..10dcbab78 100644 --- a/extensions/issuance/issuance-credentials/src/test/java/org/eclipse/edc/identityhub/issuance/credentials/rule/ExpressionCredentialRuleTest.java +++ b/extensions/issuance/issuerservice-issuance-rules/src/test/java/org/eclipse/edc/issuerservice/issuance/rules/expression/ExpressionCredentialRuleTest.java @@ -12,9 +12,9 @@ * */ -package org.eclipse.edc.identityhub.issuance.credentials.rule; +package org.eclipse.edc.issuerservice.issuance.rules.expression; -import org.eclipse.edc.identityhub.issuance.credentials.rule.ExpressionCredentialRule.Operator; +import org.eclipse.edc.issuerservice.issuance.rules.expression.ExpressionCredentialRule.Operator; import org.junit.jupiter.api.Test; import java.util.Map; diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/build.gradle.kts b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/build.gradle.kts index e0901acf1..0d9d2e15e 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/build.gradle.kts +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.edc.lib.jerseyproviders) implementation(libs.edc.lib.transform) implementation(libs.edc.dcp.transform) + implementation(libs.edc.lib.validator) implementation(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-transform-lib")) implementation(libs.jakarta.rsApi) testImplementation(libs.edc.junit) diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/IssuerApiExtension.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/IssuerApiExtension.java index 0f8d712e4..e33ee04f9 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/IssuerApiExtension.java +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/IssuerApiExtension.java @@ -16,8 +16,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequest.CredentialRequestApiController; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequest.validation.CredentialRequestMessageValidator; import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequeststatus.CredentialRequestStatusApiController; import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.issuermetadata.IssuerMetadataApiController; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromCredentialObjectTransformer; import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromCredentialRequestStatusTransformer; import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromIssuerMetadataTransformer; @@ -36,6 +39,7 @@ 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.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.jersey.providers.jsonld.JerseyJsonLdInterceptor; import org.eclipse.edc.web.jersey.providers.jsonld.ObjectMapperProvider; import org.eclipse.edc.web.spi.WebService; @@ -48,6 +52,7 @@ 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.protocols.dcp.issuer.IssuerApiExtension.NAME; +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage.CREDENTIAL_REQUEST_MESSAGE_TERM; import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0; import static org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext.ISSUANCE_API; import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; @@ -76,6 +81,15 @@ public class IssuerApiExtension implements ServiceExtension { @Inject private JsonLd jsonLd; + @Inject + private DcpIssuerService dcpIssuerService; + + @Inject + private DcpIssuerSelfIssuedTokenVerifier dcpIssuerSelfIssuedTokenVerifier; + + @Inject + private JsonObjectValidatorRegistry validatorRegistry; + @Override public void initialize(ServiceExtensionContext context) { @@ -83,9 +97,9 @@ public void initialize(ServiceExtensionContext context) { var dcpRegistry = transformerRegistry.forContext(DCP_SCOPE_V_1_0); registerTransformers(dcpRegistry, DSPACE_DCP_NAMESPACE_V_1_0); + registerValidators(DSPACE_DCP_NAMESPACE_V_1_0); - - webService.registerResource(ISSUANCE_API, new CredentialRequestApiController(dcpRegistry)); + webService.registerResource(ISSUANCE_API, new CredentialRequestApiController(dcpIssuerService, dcpIssuerSelfIssuedTokenVerifier, validatorRegistry, dcpRegistry, DSPACE_DCP_NAMESPACE_V_1_0)); webService.registerResource(ISSUANCE_API, new CredentialRequestStatusApiController(dcpRegistry)); webService.registerResource(ISSUANCE_API, new IssuerMetadataApiController(dcpRegistry)); @@ -108,6 +122,11 @@ private void registerTransformers(TypeTransformerRegistry dcpRegistry, JsonLdNam dcpRegistry.register(new JsonObjectToCredentialRequestMessageTransformer(typeManager, JSON_LD, namespace)); } + private void registerValidators(JsonLdNamespace namespace) { + + validatorRegistry.register(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM), CredentialRequestMessageValidator.instance(namespace)); + } + private void registerVersionInfo(ClassLoader resourceClassLoader) { try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) { if (versionContent == null) { diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApi.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApi.java index b5ef90fa5..c03bf0f4f 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApi.java +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApi.java @@ -56,5 +56,5 @@ public interface CredentialRequestApi { } ) - Response requestCredential(JsonObject message); + Response requestCredential(JsonObject message, String token); } diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiController.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiController.java index 3981c6090..fcbdc0e64 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiController.java +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiController.java @@ -16,27 +16,74 @@ 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.Produces; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.spi.iam.TokenRepresentation; 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 java.net.URI; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage.CREDENTIAL_REQUEST_MESSAGE_TERM; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @Path("/v1alpha/credentials") public class CredentialRequestApiController implements CredentialRequestApi { - public CredentialRequestApiController(TypeTransformerRegistry dcpRegistry) { - + private final DcpIssuerService dcpIssuerService; + private final DcpIssuerSelfIssuedTokenVerifier tokenValidator; + private final JsonObjectValidatorRegistry validatorRegistry; + private final TypeTransformerRegistry dcpTransformerRegistry; + private final JsonLdNamespace namespace; + + public CredentialRequestApiController(DcpIssuerService dcpIssuerService, + DcpIssuerSelfIssuedTokenVerifier tokenValidator, + JsonObjectValidatorRegistry validatorRegistry, + TypeTransformerRegistry dcpTransformerRegistry, + JsonLdNamespace namespace) { + this.dcpIssuerService = dcpIssuerService; + this.tokenValidator = tokenValidator; + this.validatorRegistry = validatorRegistry; + this.dcpTransformerRegistry = dcpTransformerRegistry; + this.namespace = namespace; } + @POST @Path("/") @Override - public Response requestCredential(JsonObject message) { - return Response.noContent().build(); + public Response requestCredential(JsonObject message, @HeaderParam(AUTHORIZATION) String token) { + if (token == null) { + throw new AuthenticationFailedException("Authorization header missing"); + } + token = token.replace("Bearer", "").trim(); + + validatorRegistry.validate(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM), message).orElseThrow(ValidationFailureException::new); + + var credentialMessage = dcpTransformerRegistry.transform(message, CredentialRequestMessage.class).orElseThrow(InvalidRequestException::new); + var tokenRepresentation = TokenRepresentation.Builder.newInstance().token(token).build(); + + + var participant = tokenValidator.verify(tokenRepresentation) + .orElseThrow((f) -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail()))); + + return dcpIssuerService.initiateCredentialsIssuance(credentialMessage, participant) + .map(response -> Response.created(URI.create("/v1alpha/requests/" + response.requestId())).build()) + .orElseThrow(exceptionMapper(CredentialRequestMessage.class, null)); + } } diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidator.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidator.java new file mode 100644 index 000000000..77d889fe8 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidator.java @@ -0,0 +1,35 @@ +/* + * 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.issuer.api.v1alpha.credentialrequest.validation; + +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.validator.jsonobject.JsonObjectValidator; +import org.eclipse.edc.validator.jsonobject.validators.MandatoryObject; +import org.eclipse.edc.validator.spi.Validator; + +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage.CREDENTIAL_REQUEST_MESSAGE_CREDENTIALS_TERM; + +/** + * Validator for the credential request message. + */ +public class CredentialRequestMessageValidator { + + public static Validator instance(JsonLdNamespace namespace) { + return JsonObjectValidator.newValidator() + .verify(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_CREDENTIALS_TERM), MandatoryObject::new) + .build(); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json index 30622a4ff..761c8d2a6 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/main/resources/issuer-api-version.json @@ -2,7 +2,7 @@ { "version": "1.0.0", "urlPath": "/v1alpha", - "lastUpdated": "2025-02-07T08:00:00Z", + "lastUpdated": "2025-02-11T08:00:00Z", "maturity": null } ] \ No newline at end of file diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiControllerTest.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiControllerTest.java new file mode 100644 index 000000000..d37f2def7 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/CredentialRequestApiControllerTest.java @@ -0,0 +1,189 @@ +/* + * 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.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequest; + +import com.nimbusds.jwt.JWTClaimsSet; +import jakarta.json.JsonObject; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequest; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.NotAuthorizedException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage.CREDENTIAL_REQUEST_MESSAGE_TERM; +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.ValidationResult.failure; +import static org.eclipse.edc.validator.spi.ValidationResult.success; +import static org.eclipse.edc.validator.spi.Violation.violation; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ApiTest +@SuppressWarnings("resource") +class CredentialRequestApiControllerTest extends RestControllerTestBase { + + private final JsonObjectValidatorRegistry validatorRegistryMock = mock(); + private final TypeTransformerRegistry typeTransformerRegistry = mock(); + private final DcpIssuerService dcpIssuerService = mock(); + private final DcpIssuerSelfIssuedTokenVerifier dcpIssuerTokenVerifier = mock(); + private final JsonLdNamespace namespace = DSPACE_DCP_NAMESPACE_V_1_0; + + @Test + void requestCredential_tokenNotPresent_shouldReturn401() { + assertThatThrownBy(() -> controller().requestCredential(createObjectBuilder().build(), null)) + .isInstanceOf(AuthenticationFailedException.class) + .hasMessage("Authorization header missing"); + + verifyNoInteractions(dcpIssuerService, dcpIssuerTokenVerifier, typeTransformerRegistry); + + } + + @Test + void requestCredential_validationError_shouldReturn400() { + when(validatorRegistryMock.validate(eq(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM)), any())).thenReturn(failure(violation("foo", "bar"))); + + assertThatThrownBy(() -> controller().requestCredential(createObjectBuilder().build(), generateJwt())) + .isInstanceOf(ValidationFailureException.class) + .hasMessage("foo"); + verifyNoInteractions(dcpIssuerService, dcpIssuerTokenVerifier, typeTransformerRegistry); + + } + + @Test + void requestCredential_transformationError_shouldReturn400() { + when(validatorRegistryMock.validate(eq(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM)), any())).thenReturn(success()); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(CredentialRequestMessage.class))).thenReturn(Result.failure("cannot transform")); + + assertThatThrownBy(() -> controller().requestCredential(createObjectBuilder().build(), generateJwt())) + .isInstanceOf(InvalidRequestException.class) + .hasMessage("cannot transform"); + + verifyNoInteractions(dcpIssuerService, dcpIssuerTokenVerifier); + } + + @Test + void requestCredential_tokenVerificationFails_shouldReturn401() { + when(validatorRegistryMock.validate(eq(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM)), any())).thenReturn(success()); + var requestMessage = createCredentialRequestMessage(); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(CredentialRequestMessage.class))).thenReturn(Result.success(requestMessage)); + when(dcpIssuerTokenVerifier.verify(any())).thenReturn(ServiceResult.unauthorized("unauthorized")); + + assertThatThrownBy(() -> controller().requestCredential(createObjectBuilder().build(), generateJwt())) + .isExactlyInstanceOf(AuthenticationFailedException.class) + .hasMessageContaining("unauthorized"); + + verifyNoInteractions(dcpIssuerService); + } + + @Test + void requestCredential_initiateCredentialIssuanceFails_shouldReturn_401() { + when(validatorRegistryMock.validate(eq(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM)), any())).thenReturn(success()); + var requestMessage = createCredentialRequestMessage(); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(CredentialRequestMessage.class))).thenReturn(Result.success(requestMessage)); + var participant = new Participant("id", "did", "name"); + + var ctx = new DcpRequestContext(participant, Map.of()); + var token = generateJwt(); + when(dcpIssuerTokenVerifier.verify(any())).thenReturn(ServiceResult.success(ctx)); + when(dcpIssuerService.initiateCredentialsIssuance(any(), any())).thenReturn(ServiceResult.unauthorized("cannot initiate unauthorized")); + + assertThatThrownBy(() -> controller().requestCredential(createObjectBuilder().build(), token)) + .isExactlyInstanceOf(NotAuthorizedException.class) + .hasMessage("cannot initiate unauthorized"); + + verify(dcpIssuerTokenVerifier).verify(argThat(tr -> tr.getToken().equals(token))); + verify(dcpIssuerService).initiateCredentialsIssuance(requestMessage, ctx); + } + + @Test + void requestCredential() { + when(validatorRegistryMock.validate(eq(namespace.toIri(CREDENTIAL_REQUEST_MESSAGE_TERM)), any())).thenReturn(success()); + var requestMessage = createCredentialRequestMessage(); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(CredentialRequestMessage.class))).thenReturn(Result.success(requestMessage)); + var participant = new Participant("id", "did", "name"); + var ctx = new DcpRequestContext(participant, Map.of()); + + var token = generateJwt(); + var responseMessage = new CredentialRequestMessage.Response(UUID.randomUUID().toString()); + when(dcpIssuerTokenVerifier.verify(any())).thenReturn(ServiceResult.success(ctx)); + when(dcpIssuerService.initiateCredentialsIssuance(any(), any())).thenReturn(ServiceResult.success(responseMessage)); + + var response = controller().requestCredential(createObjectBuilder().build(), token); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getHeaderString("Location")).contains("/v1alpha/requests/%s".formatted(responseMessage.requestId())); + + verify(dcpIssuerTokenVerifier).verify(argThat(tr -> tr.getToken().equals(token))); + verify(dcpIssuerService).initiateCredentialsIssuance(requestMessage, ctx); + } + + @Override + protected CredentialRequestApiController controller() { + return new CredentialRequestApiController(dcpIssuerService, dcpIssuerTokenVerifier, validatorRegistryMock, typeTransformerRegistry, namespace); + } + + + private CredentialRequestMessage createCredentialRequestMessage() { + return createCredentialRequestMessageBuilder() + .credential(new CredentialRequest("test-credential1", "test-issuer1", null)) + .build(); + } + + private CredentialRequestMessage.Builder createCredentialRequestMessageBuilder() { + return CredentialRequestMessage.Builder.newInstance(); + } + + 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-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidatorTest.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidatorTest.java new file mode 100644 index 000000000..b3f57344d --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-api/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/api/v1alpha/credentialrequest/validation/CredentialRequestMessageValidatorTest.java @@ -0,0 +1,48 @@ +/* + * 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.issuer.api.v1alpha.credentialrequest.validation; + +import jakarta.json.JsonObject; +import org.eclipse.edc.validator.spi.Validator; +import org.junit.jupiter.api.Test; + +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; +import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage.CREDENTIAL_REQUEST_MESSAGE_CREDENTIALS_TERM; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +public class CredentialRequestMessageValidatorTest { + private final Validator validator = CredentialRequestMessageValidator.instance(DSPACE_DCP_NAMESPACE_V_1_0); + + + @Test + void validate_success() { + var jo = createObjectBuilder() + .add(DSPACE_DCP_NAMESPACE_V_1_0.toIri(CREDENTIAL_REQUEST_MESSAGE_CREDENTIALS_TERM), createArrayBuilder() + .add(createObjectBuilder())) + .build(); + + assertThat(validator.validate(jo)).isSucceeded(); + } + + @Test + void validate_missingCredentials() { + var jo = createObjectBuilder() + .build(); + + assertThat(validator.validate(jo)).isFailed(); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/build.gradle.kts b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/build.gradle.kts new file mode 100644 index 000000000..004819e34 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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` +} + +dependencies { + api(project(":spi:verifiable-credential-spi")) + api(project(":spi:issuerservice:issuerservice-participant-spi")) + api(project(":spi:issuerservice:issuerservice-issuance-spi")) + api(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-spi")) + api(libs.edc.spi.jwt) + api(libs.edc.spi.identity.did) + api(libs.edc.spi.transaction) + implementation(libs.edc.vc.jwt) + implementation(libs.edc.lib.token) + implementation(libs.nimbus.jwt) + testImplementation(libs.edc.junit) + testImplementation(testFixtures(project(":spi:verifiable-credential-spi"))) +} + diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpAttestationContext.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpAttestationContext.java new file mode 100644 index 000000000..47df04256 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpAttestationContext.java @@ -0,0 +1,36 @@ +/* + * 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.issuer; + + +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationContext; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.jetbrains.annotations.Nullable; + +/** + * Attestation context for DCP. + */ +public record DcpAttestationContext(DcpRequestContext context) implements AttestationContext { + @Override + public @Nullable ClaimToken getClaimToken(String type) { + return context.claims().get(type); + } + + @Override + public String participantId() { + return context.participant().participantId(); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java new file mode 100644 index 000000000..7f031af81 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java @@ -0,0 +1,106 @@ +/* + * 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.issuer; + +import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; +import org.eclipse.edc.iam.identitytrust.spi.validation.TokenValidationAction; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationPipeline; +import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; +import org.eclipse.edc.issuerservice.spi.participant.store.ParticipantStore; +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.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.token.rules.AudienceValidationRule; +import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule; +import org.eclipse.edc.token.spi.TokenValidationRulesRegistry; +import org.eclipse.edc.token.spi.TokenValidationService; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.eclipse.edc.verifiablecredentials.jwt.rules.IssuerEqualsSubjectRule; +import org.jetbrains.annotations.NotNull; + +import java.time.Clock; + +@Extension("DCP Issuer Core Extension") +public class DcpIssuerCoreExtension implements ServiceExtension { + + public static final String DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT = "dcp-issuer-si"; + + + @Setting(description = "DID of this issuer", key = "edc.issuer.id") + private String issuerId; + + @Inject + private TokenValidationRulesRegistry rulesRegistry; + + @Inject + private TokenValidationService tokenValidationService; + + @Inject + private ParticipantStore participantStore; + + @Inject + private DidPublicKeyResolver didPublicKeyResolver; + + @Inject + private CredentialDefinitionService credentialDefinitionService; + + @Inject + private IssuanceProcessStore issuanceProcessStore; + + @Inject + private AttestationPipeline attestationPipeline; + + @Inject + private CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator; + + @Inject + private TransactionContext transactionContext; + + @Inject + private Clock clock; + + + @Override + public void initialize(ServiceExtensionContext context) { + rulesRegistry.addRule(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT, new IssuerEqualsSubjectRule()); + rulesRegistry.addRule(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT, new AudienceValidationRule(issuerId)); + rulesRegistry.addRule(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT, new ExpirationIssuedAtValidationRule(clock, 5, false)); + } + + @Provider + public DcpIssuerService createIssuerService() { + return new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator); + } + + @Provider + public DcpIssuerSelfIssuedTokenVerifier createTokenVerifier() { + return new DcpIssuerSelfIssuedTokenVerifierImpl(participantStore, tokenValidationAction()); + } + + @NotNull + private TokenValidationAction tokenValidationAction() { + return (tokenRepresentation) -> { + var rules = rulesRegistry.getRules(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT); + return tokenValidationService.validate(tokenRepresentation, didPublicKeyResolver, rules); + }; + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImpl.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImpl.java new file mode 100644 index 000000000..dd82bd5c3 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImpl.java @@ -0,0 +1,81 @@ +/* + * 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.issuer; + +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.iam.identitytrust.spi.validation.TokenValidationAction; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.issuerservice.spi.participant.store.ParticipantStore; +import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.text.ParseException; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +public class DcpIssuerSelfIssuedTokenVerifierImpl implements DcpIssuerSelfIssuedTokenVerifier { + + private final ParticipantStore store; + private final TokenValidationAction tokenValidation; + + public DcpIssuerSelfIssuedTokenVerifierImpl(ParticipantStore store, TokenValidationAction tokenValidation) { + this.store = store; + this.tokenValidation = tokenValidation; + } + + + @Override + public ServiceResult verify(TokenRepresentation tokenRepresentation) { + return getTokenIssuer(tokenRepresentation.getToken()) + .compose(this::getParticipant) + .compose(participant -> validateToken(tokenRepresentation, participant)); + } + + private ServiceResult getTokenIssuer(String token) { + try { + return Optional.ofNullable(SignedJWT.parse(token).getJWTClaimsSet().getClaim(JwtRegisteredClaimNames.ISSUER)) + .map(Object::toString) + .map(ServiceResult::success) + .orElseGet(() -> ServiceResult.unauthorized("Issuer claim not present")); + } catch (ParseException e) { + return ServiceResult.badRequest("Failed to decode token"); + } + } + + private ServiceResult getParticipant(String issuer) { + var query = QuerySpec.Builder.newInstance().filter(Criterion.criterion("did", "=", issuer)).build(); + return ServiceResult.from(store.query(query)).compose(this::findFirst); + } + + private ServiceResult findFirst(Collection participants) { + return participants.stream().findFirst() + .map(ServiceResult::success) + .orElseGet(() -> ServiceResult.unauthorized("Participant not found")); + } + + private ServiceResult validateToken(TokenRepresentation token, Participant participant) { + var res = tokenValidation.apply(token); + if (res.failed()) { + return ServiceResult.unauthorized("Token validation failed"); + } + return ServiceResult.success(new DcpRequestContext(participant, Map.of())); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java new file mode 100644 index 000000000..164805983 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java @@ -0,0 +1,143 @@ +/* + * 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.issuer; + +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequest; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationPipeline; +import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcess; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcessStates; +import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +public class DcpIssuerServiceImpl implements DcpIssuerService { + + private final TransactionContext transactionContext; + private final CredentialDefinitionService credentialDefinitionService; + private final IssuanceProcessStore issuanceProcessStore; + private final AttestationPipeline attestationPipeline; + private final CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator; + + + public DcpIssuerServiceImpl(TransactionContext transactionContext, + CredentialDefinitionService credentialDefinitionService, + IssuanceProcessStore issuanceProcessStore, + AttestationPipeline attestationPipeline, + CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator) { + this.transactionContext = transactionContext; + this.credentialDefinitionService = credentialDefinitionService; + this.issuanceProcessStore = issuanceProcessStore; + this.attestationPipeline = attestationPipeline; + this.credentialRuleDefinitionEvaluator = credentialRuleDefinitionEvaluator; + } + + @Override + public ServiceResult initiateCredentialsIssuance(CredentialRequestMessage message, DcpRequestContext context) { + if (message.getCredentials().isEmpty()) { + return ServiceResult.badRequest("No credentials requested"); + } + return transactionContext.execute(() -> getCredentialsDefinitions(message) + .compose(credentialDefinitions -> evaluateAttestations(context, credentialDefinitions)) + .compose(this::evaluateRules) + .compose(evaluation -> createIssuanceProcess(context, evaluation)) + .map(issuanceProcess -> new CredentialRequestMessage.Response(issuanceProcess.getId()))); + + } + + + private ServiceResult> getCredentialsDefinitions(CredentialRequestMessage message) { + + var credentialTypes = message.getCredentials().stream() + .map(CredentialRequest::credentialType) + .collect(Collectors.toSet()); + + var query = QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("credentialType", "in", credentialTypes)) + .build(); + + return credentialDefinitionService.queryCredentialDefinitions(query) + .compose(credentialDefinitions -> validateCredentialDefinitions(message, credentialDefinitions)); + } + + private ServiceResult> validateCredentialDefinitions(CredentialRequestMessage message, Collection credentialDefinitions) { + if (message.getCredentials().size() != credentialDefinitions.size()) { + return ServiceResult.badRequest("Not all requested credential types have a corresponding credential definition"); + } + return ServiceResult.success(credentialDefinitions); + } + + private ServiceResult evaluateAttestations(DcpRequestContext context, Collection credentialDefinitions) { + + var attestations = credentialDefinitions.stream() + .flatMap(credentialDefinition -> credentialDefinition.getAttestations().stream()) + .collect(Collectors.toSet()); + + if (attestations.isEmpty()) { + return ServiceResult.badRequest("No attestations found for requested credentials"); + } + + var result = attestationPipeline.evaluate(attestations, new DcpAttestationContext(context)); + if (result.failed()) { + return ServiceResult.unauthorized("unauthorized"); + } + return ServiceResult.success(new AttestationEvaluationResponse(credentialDefinitions, result.getContent())); + } + + private ServiceResult evaluateRules(AttestationEvaluationResponse evaluationResponse) { + + var credentialRuleDefinitions = evaluationResponse.credentialDefinitions().stream() + .flatMap(credentialDefinition -> credentialDefinition.getRules().stream()) + .collect(Collectors.toList()); + + var result = credentialRuleDefinitionEvaluator.evaluate(credentialRuleDefinitions, evaluationResponse::claims); + if (result.failed()) { + return ServiceResult.unauthorized("unauthorized"); + } + return ServiceResult.success(evaluationResponse); + } + + private ServiceResult createIssuanceProcess(DcpRequestContext context, AttestationEvaluationResponse evaluationResponse) { + + var credentialDefinitionIds = evaluationResponse.credentialDefinitions().stream() + .map(CredentialDefinition::getId) + .collect(Collectors.toSet()); + + var issuanceProcess = IssuanceProcess.Builder.newInstance() + .participantId(context.participant().participantId()) + .state(IssuanceProcessStates.APPROVED.code()) + .credentialDefinitions(credentialDefinitionIds) + .claims(evaluationResponse.claims()) + .build(); + + issuanceProcessStore.save(issuanceProcess); + return ServiceResult.success(issuanceProcess); + } + + private record AttestationEvaluationResponse(Collection credentialDefinitions, + Map claims) { + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..aed67b991 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/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.protocols.dcp.issuer.DcpIssuerCoreExtension \ No newline at end of file diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtensionTest.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtensionTest.java new file mode 100644 index 000000000..df3e2798a --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtensionTest.java @@ -0,0 +1,71 @@ +/* + * 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.issuer; + +import org.eclipse.edc.boot.system.injection.ObjectFactory; +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.eclipse.edc.token.rules.AudienceValidationRule; +import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule; +import org.eclipse.edc.token.spi.TokenValidationRulesRegistry; +import org.eclipse.edc.verifiablecredentials.jwt.rules.IssuerEqualsSubjectRule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.identityhub.protocols.dcp.issuer.DcpIssuerCoreExtension.DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class DcpIssuerCoreExtensionTest { + + + private final TokenValidationRulesRegistry tokenValidationRulesRegistry = mock(); + + @BeforeEach + void setUp(ServiceExtensionContext context) { + context.registerService(TokenValidationRulesRegistry.class, tokenValidationRulesRegistry); + } + + @Test + void verifyProviders(ServiceExtensionContext context, ObjectFactory factory) { + when(context.getConfig()).thenReturn(ConfigFactory.fromMap(Map.of("edc.issuer.id", "did::web:issuer"))); + + var extension = factory.constructInstance(DcpIssuerCoreExtension.class); + assertThat(extension.createIssuerService()).isInstanceOf(DcpIssuerServiceImpl.class); + assertThat(extension.createTokenVerifier()).isInstanceOf(DcpIssuerSelfIssuedTokenVerifierImpl.class); + } + + @Test + void verifyTokenValidationRules(ServiceExtensionContext context, ObjectFactory factory) { + + when(context.getConfig()).thenReturn(ConfigFactory.fromMap(Map.of("edc.issuer.id", "did::web:issuer"))); + + var extension = factory.constructInstance(DcpIssuerCoreExtension.class); + extension.initialize(context); + + verify(tokenValidationRulesRegistry).addRule(eq(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT), isA(IssuerEqualsSubjectRule.class)); + verify(tokenValidationRulesRegistry).addRule(eq(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT), isA(AudienceValidationRule.class)); + verify(tokenValidationRulesRegistry).addRule(eq(DCP_ISSUER_SELF_ISSUED_TOKEN_CONTEXT), isA(ExpirationIssuedAtValidationRule.class)); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImplTest.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImplTest.java new file mode 100644 index 000000000..27fd9488f --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerSelfIssuedTokenVerifierImplTest.java @@ -0,0 +1,108 @@ +/* + * 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.issuer; + +import com.nimbusds.jose.jwk.ECKey; +import org.eclipse.edc.iam.identitytrust.spi.validation.TokenValidationAction; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerSelfIssuedTokenVerifier; +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.issuerservice.spi.participant.store.ParticipantStore; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.StoreResult; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Map; + +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil.generateJwt; +import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.generateEcKey; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DcpIssuerSelfIssuedTokenVerifierImplTest { + + public static final String ISSUER_DID = "did:web:issuer"; + public static final String PARTICIPANT_DID = "did:web:participant"; + public static final String DID_WEB_PARTICIPANT_KEY_1 = "did:web:participant#key1"; + public static final ECKey PARTICIPANT_KEY = generateEcKey(DID_WEB_PARTICIPANT_KEY_1); + + private final TokenValidationAction tokenValidationAction = mock(); + private final ParticipantStore participantStore = mock(); + private final DcpIssuerSelfIssuedTokenVerifier dcpIssuerTokenVerifier = new DcpIssuerSelfIssuedTokenVerifierImpl(participantStore, tokenValidationAction); + + + @Test + void verify() { + + var token = TokenRepresentation.Builder.newInstance().token(generateToken()).build(); + + when(participantStore.query(any())).thenReturn(StoreResult.success(List.of(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, PARTICIPANT_DID)))); + when(tokenValidationAction.apply(token)).thenReturn(Result.success(ClaimToken.Builder.newInstance().build())); + + var result = dcpIssuerTokenVerifier.verify(token); + + assertThat(result).isSucceeded(); + Mockito.verify(participantStore).query(argThat(qs -> qs.getFilterExpression().stream().anyMatch(c -> c.getOperandRight().equals(PARTICIPANT_DID)))); + + } + + @Test + void verify_participantNotFound() { + + var token = TokenRepresentation.Builder.newInstance().token(generateToken()).build(); + + when(participantStore.query(any())).thenReturn(StoreResult.success(List.of())); + + var result = dcpIssuerTokenVerifier.verify(token); + + assertThat(result).isFailed(); + + } + + @Test + void verify_tokenValidationFails() { + + var token = TokenRepresentation.Builder.newInstance().token(generateToken()).build(); + + when(participantStore.query(any())).thenReturn(StoreResult.success(List.of(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, PARTICIPANT_DID)))); + when(tokenValidationAction.apply(token)).thenReturn(Result.failure("failed")); + + var result = dcpIssuerTokenVerifier.verify(token); + + assertThat(result).isFailed(); + + } + + @Test + void verify_faultyToken() { + + var token = TokenRepresentation.Builder.newInstance().token("faultyToken").build(); + + var result = dcpIssuerTokenVerifier.verify(token); + + assertThat(result).isFailed(); + + } + + private String generateToken() { + return generateJwt(ISSUER_DID, PARTICIPANT_DID, PARTICIPANT_DID, Map.of(), PARTICIPANT_KEY); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java new file mode 100644 index 000000000..64f0e23fb --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java @@ -0,0 +1,103 @@ +/* + * 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.issuer; + +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.DcpIssuerService; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequest; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationPipeline; +import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcess; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcessStates; +import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; +import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.NoopTransactionContext; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DcpIssuerServiceImplTest { + + private final AttestationPipeline attestationPipeline = mock(); + private final CredentialDefinitionService credentialDefinitionService = mock(); + private final TransactionContext transactionContext = new NoopTransactionContext(); + private final IssuanceProcessStore issuanceProcessStore = mock(); + private final CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator = mock(); + + private final DcpIssuerService dcpIssuerService = new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator); + + + @Test + void initiateCredentialsIssuance() { + + var message = CredentialRequestMessage.Builder.newInstance() + .credential(new CredentialRequest("MembershipCredential", "format", null)) + .build(); + + var attestations = Set.of("attestation1", "attestation2"); + + var credentialRuleDefinition = new CredentialRuleDefinition("expression", Map.of()); + var credentialDefinition = CredentialDefinition.Builder.newInstance() + .id("credentialDefinitionId") + .credentialType("MembershipCredential") + .jsonSchema("jsonSchema") + .jsonSchemaUrl("jsonSchemaUrl") + .attestations(attestations) + .rule(credentialRuleDefinition) + .build(); + + var participant = new DcpRequestContext(new Participant("participantId", "participantDid", "name"), Map.of()); + + Map claims = Map.of("claim1", "value1", "claim2", "value2"); + + when(credentialDefinitionService.queryCredentialDefinitions(any())).thenReturn(ServiceResult.success(List.of(credentialDefinition))); + when(attestationPipeline.evaluate(eq(attestations), any())).thenReturn(Result.success(claims)); + when(credentialRuleDefinitionEvaluator.evaluate(eq(List.of(credentialRuleDefinition)), any())).thenReturn(Result.success()); + + var result = dcpIssuerService.initiateCredentialsIssuance(message, participant); + + assertThat(result).isSucceeded(); + + var captor = ArgumentCaptor.forClass(IssuanceProcess.class); + verify(issuanceProcessStore).save(captor.capture()); + + var issuanceProcess = captor.getValue(); + + assertThat(issuanceProcess).isNotNull(); + assertThat(issuanceProcess.getId()).isEqualTo(result.getContent().requestId()); + assertThat(issuanceProcess.getCredentialDefinitions()).containsExactly("credentialDefinitionId"); + assertThat(issuanceProcess.getParticipantId()).isEqualTo("participantId"); + assertThat(issuanceProcess.getState()).isEqualTo(IssuanceProcessStates.APPROVED.code()); + assertThat(issuanceProcess.getClaims()).containsAllEntriesOf(claims); + } +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/build.gradle.kts b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/build.gradle.kts index 5545bc0b8..8d7ff84f8 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/build.gradle.kts +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/build.gradle.kts @@ -19,5 +19,6 @@ plugins { dependencies { api(project(":spi:verifiable-credential-spi")) + api(project(":spi:issuerservice:issuerservice-participant-spi")) } diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerSelfIssuedTokenVerifier.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerSelfIssuedTokenVerifier.java new file mode 100644 index 000000000..3506b5675 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerSelfIssuedTokenVerifier.java @@ -0,0 +1,31 @@ +/* + * 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.issuer.spi; + +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.ServiceResult; + +/** + * Service interface for validating the SI token received from a participant request. + */ + +@ExtensionPoint +public interface DcpIssuerSelfIssuedTokenVerifier { + + ServiceResult verify(TokenRepresentation tokenRepresentation); + +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerService.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerService.java new file mode 100644 index 000000000..88322748f --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/DcpIssuerService.java @@ -0,0 +1,34 @@ +/* + * 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.issuer.spi; + +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.CredentialRequestMessage; +import org.eclipse.edc.identityhub.protocols.dcp.issuer.spi.model.DcpRequestContext; +import org.eclipse.edc.spi.result.ServiceResult; + +/** + * Service interface for the DCP Issuer service. + */ +public interface DcpIssuerService { + + /** + * Initiates the issuance of credentials. + * + * @param message the credential request message + * @param context the DCP request context + * @return the result of the issuance initiation + */ + ServiceResult initiateCredentialsIssuance(CredentialRequestMessage message, DcpRequestContext context); +} diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/CredentialRequestMessage.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/CredentialRequestMessage.java index 099dab6e4..68125e272 100644 --- a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/CredentialRequestMessage.java +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/CredentialRequestMessage.java @@ -66,4 +66,11 @@ public CredentialRequestMessage build() { return instance; } } + + /** + * Represents a response for a {@link CredentialRequestMessage} + */ + public record Response(String requestId) { + + } } diff --git a/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/DcpRequestContext.java b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/DcpRequestContext.java new file mode 100644 index 000000000..b84ee2444 --- /dev/null +++ b/extensions/protocols/dcp/dcp-issuer/dcp-issuer-spi/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/spi/model/DcpRequestContext.java @@ -0,0 +1,28 @@ +/* + * 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.issuer.spi.model; + +import org.eclipse.edc.issuerservice.spi.participant.model.Participant; +import org.eclipse.edc.spi.iam.ClaimToken; + +import java.util.Map; + +/** + * Context for a DCP request. Contains the {@link Participant} and a set of claims + * that might come from a DCP presentation request. + */ +public record DcpRequestContext(Participant participant, Map claims) { + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index daea93b40..30329d1a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,7 @@ edc-lib-token = { module = "org.eclipse.edc:token-lib", version.ref = "edc" } edc-lib-transform = { module = "org.eclipse.edc:transform-lib", version.ref = "edc" } edc-lib-util = { module = "org.eclipse.edc:util-lib", version.ref = "edc" } edc-lib-statemachine = { module = "org.eclipse.edc:state-machine-lib", version.ref = "edc" } +edc-lib-validator = { module = "org.eclipse.edc:validator-lib", version.ref = "edc" } # SQL dependencies edc-sql-bootstrapper = { module = "org.eclipse.edc:sql-bootstrapper", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9a1c63257..75e74c185 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -73,6 +73,7 @@ include(":extensions:protocols:dcp:dcp-spi") include(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-spi") 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") @@ -99,7 +100,8 @@ include(":extensions:api:identity-api:validators:participant-context-validators" include(":extensions:api:identity-api:validators:verifiable-credential-validators") // issuance modules -include(":extensions:issuance:issuance-credentials") +include(":extensions:issuance:issuerservice-issuance-attestations") +include(":extensions:issuance:issuerservice-issuance-rules") // other modules include(":launcher:identityhub") diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/rule/CredentialRuleDefinitionEvaluator.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/rule/CredentialRuleDefinitionEvaluator.java new file mode 100644 index 000000000..dd147c9ae --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/rule/CredentialRuleDefinitionEvaluator.java @@ -0,0 +1,38 @@ +/* + * 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.issuerservice.spi.issuance.rule; + +import org.eclipse.edc.issuerservice.spi.issuance.IssuanceContext; +import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.result.Result; + +import java.util.Collection; + +/** + * Evaluates a collection of credential rule definitions. + */ +@ExtensionPoint +public interface CredentialRuleDefinitionEvaluator { + + /** + * Evaluates a collection of credential rule definitions. + * + * @param definitions the definitions to evaluate + * @param context the issuance context + * @return the result of the evaluation + */ + Result evaluate(Collection definitions, IssuanceContext context); +}