diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceCoreExtension.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceCoreExtension.java index 89777b1b1..259fd1c53 100644 --- a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceCoreExtension.java +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceCoreExtension.java @@ -20,6 +20,7 @@ import org.eclipse.edc.issuerservice.spi.credentials.CredentialStatusService; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore; import org.eclipse.edc.issuerservice.spi.issuance.delivery.CredentialStorageClient; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGeneratorRegistry; import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessManager; import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessService; @@ -84,9 +85,13 @@ public class IssuanceCoreExtension implements ServiceExtension { @Inject private TransactionContext transactionContext; + @Inject private CredentialStatusService credentialStatusService; + @Inject + private IssuanceObservable issuanceObservable; + @Provider public IssuanceProcessManager createIssuanceProcessManager() { @@ -106,11 +111,13 @@ public IssuanceProcessManager createIssuanceProcessManager() { .credentialStorageClient(credentialStorageClient) .credentialStatusService(credentialStatusService) .entityRetryProcessConfiguration(stateMachineConfiguration.entityRetryProcessConfiguration()) + .observable(issuanceObservable) .build(); } return issuanceProcessManager; } + @Provider public IssuanceProcessService createIssuanceProcessService() { return new IssuanceProcessServiceImpl(transactionContext, issuanceProcessStore); 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 f74eb9b9b..c5d3aef6f 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 @@ -21,6 +21,8 @@ import org.eclipse.edc.issuerservice.issuance.attestation.AttestationDefinitionValidatorRegistryImpl; import org.eclipse.edc.issuerservice.issuance.attestation.AttestationPipelineImpl; import org.eclipse.edc.issuerservice.issuance.credentialdefinition.CredentialDefinitionServiceImpl; +import org.eclipse.edc.issuerservice.issuance.events.IssuanceEventPublisher; +import org.eclipse.edc.issuerservice.issuance.events.IssuanceObservableImpl; import org.eclipse.edc.issuerservice.issuance.generator.CredentialGeneratorRegistryImpl; import org.eclipse.edc.issuerservice.issuance.generator.JoseVcdm20CredentialGenerator; import org.eclipse.edc.issuerservice.issuance.generator.JwtCredentialGenerator; @@ -36,6 +38,7 @@ 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.events.IssuanceObservable; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGeneratorRegistry; import org.eclipse.edc.issuerservice.spi.issuance.mapping.IssuanceClaimsMapper; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; @@ -45,6 +48,7 @@ import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.event.EventRouter; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.token.JwtGenerationService; import org.eclipse.edc.transaction.spi.TransactionContext; @@ -80,6 +84,9 @@ public class IssuanceServicesExtension implements ServiceExtension { @Inject private IdentityHubParticipantContextService participantContextService; + @Inject + private EventRouter eventRouter; + private AttestationPipelineImpl attestationPipeline; private CredentialRuleFactoryRegistry ruleFactoryRegistry; @@ -89,6 +96,7 @@ public class IssuanceServicesExtension implements ServiceExtension { private AttestationDefinitionValidatorRegistry attestationDefinitionValidatorRegistry; private IssuanceClaimsMapper issuanceClaimsMapper; + private IssuanceObservable issuanceObservable; @Provider public CredentialDefinitionService createParticipantService() { @@ -159,6 +167,16 @@ public AttestationDefinitionValidatorRegistry createAttestationDefinitionValidat return attestationDefinitionValidatorRegistry; } + + @Provider + public IssuanceObservable issuanceObservable() { + if (issuanceObservable == null) { + issuanceObservable = new IssuanceObservableImpl(); + issuanceObservable().registerListener(new IssuanceEventPublisher(clock, eventRouter)); + } + return issuanceObservable; + } + private AttestationPipelineImpl createAttestationPipelineImpl() { if (attestationPipeline == null) { attestationPipeline = new AttestationPipelineImpl(attestationDefinitionStore); diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceEventPublisher.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceEventPublisher.java new file mode 100644 index 000000000..1917a1390 --- /dev/null +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceEventPublisher.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.issuance.events; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.issuerservice.spi.issuance.events.CredentialDelivered; +import org.eclipse.edc.issuerservice.spi.issuance.events.CredentialGenerated; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceApproved; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEvent; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEventListener; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceProcessErrored; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceRejected; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceRequested; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcess; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventRouter; + +import java.time.Clock; +import java.util.Collection; + +public class IssuanceEventPublisher implements IssuanceEventListener { + private final Clock clock; + private final EventRouter eventRouter; + + public IssuanceEventPublisher(Clock clock, EventRouter eventRouter) { + this.clock = clock; + this.eventRouter = eventRouter; + } + + @Override + public void received(IssuanceProcess ip) { + var event = baseBuilder(IssuanceRequested.Builder.newInstance(), ip) + .credentialDefinitionIds(ip.getCredentialDefinitions()) + .credentialFormats(ip.getCredentialFormats()) + .build(); + + publishEvent(event); + } + + @Override + public void rejected(String holderPid, String issuerParticipantContextId, String failureDetail) { + var event = IssuanceRejected.Builder.newInstance() + .holderId(holderPid) + .issuerParticipantContextId(issuerParticipantContextId) + .reason(failureDetail) + .build(); + publishEvent(event); + } + + @Override + public void approved(IssuanceProcess process) { + var event = baseBuilder(IssuanceApproved.Builder.newInstance(), process) + .build(); + publishEvent(event); + } + + @Override + public void generated(IssuanceProcess process, Collection creds) { + var event = baseBuilder(CredentialGenerated.Builder.newInstance(), process) + .credentials(creds.stream().map(VerifiableCredentialContainer::credential).toList()) + .build(); + publishEvent(event); + } + + @Override + public void delivered(IssuanceProcess process, Collection credentials) { + var event = baseBuilder(CredentialDelivered.Builder.newInstance(), process) + .credentials(credentials.stream().map(VerifiableCredentialContainer::credential).toList()) + .build(); + publishEvent(event); + } + + @Override + public void errored(IssuanceProcess process, Throwable throwable) { + var event = baseBuilder(IssuanceProcessErrored.Builder.newInstance(), process) + .errorMessage(throwable.getMessage()) + .build(); + publishEvent(event); + } + + private > B baseBuilder(B builder, IssuanceProcess ip) { + builder.holderId(ip.getHolderId()) + .issuerParticipantContextId(ip.getParticipantContextId()) + .holderProcessId(ip.getHolderPid()); + return builder; + } + + @SuppressWarnings("unchecked") + private void publishEvent(IssuanceEvent event) { + var envelope = EventEnvelope.Builder.newInstance() + .payload(event) + .at(clock.millis()) + .build(); + eventRouter.publish(envelope); + } +} diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceObservableImpl.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceObservableImpl.java new file mode 100644 index 000000000..c5567bf72 --- /dev/null +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/events/IssuanceObservableImpl.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.issuance.events; + +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEventListener; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; +import org.eclipse.edc.spi.observe.ObservableImpl; + +public class IssuanceObservableImpl extends ObservableImpl implements IssuanceObservable { +} diff --git a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImpl.java b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImpl.java index f986c4d0f..6e5352124 100644 --- a/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImpl.java +++ b/core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImpl.java @@ -24,6 +24,7 @@ import org.eclipse.edc.issuerservice.spi.credentials.CredentialStatusService; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore; import org.eclipse.edc.issuerservice.spi.issuance.delivery.CredentialStorageClient; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGenerationRequest; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGeneratorRegistry; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition; @@ -53,6 +54,7 @@ public class IssuanceProcessManagerImpl extends AbstractStateEntityManager implements IssuanceProcessManager { + private IssuanceObservable observable; private CredentialGeneratorRegistry credentialGenerator; private CredentialDefinitionStore credentialDefinitionStore; private CredentialStore credentialStore; @@ -73,12 +75,16 @@ protected StateMachineManager.Builder configureStateMachineManager(StateMachineM */ @WithSpan(value = "issuance.approved") private CompletableFuture> processApproved(IssuanceProcess process) { + observable.invokeForEach(l -> l.approved(process)); return entityRetryProcessFactory.retryProcessor(process) .doProcess(result("Generate Credentials", (p, result) -> generateCredential(p))) .doProcess(result("Add Credentials to StatusList", this::addCredentialsToStatusList)) .doProcess(result("Deliver Credentials", this::deliverCredentials)) .doProcess(result("Store Credentials", this::storeCredential)) - .onSuccess((t, response) -> transitionToDelivered(t)) + .onSuccess((t, credentials) -> { + transitionToDelivered(t); + observable.invokeForEach(l -> l.delivered(process, credentials)); + }) .onFailure((t, throwable) -> transitionToApproved(t)) .onFinalFailure(this::transitionToError) .execute(); @@ -108,7 +114,8 @@ private StatusResult> addCredentialsTo private StatusResult> generateCredential(IssuanceProcess process) { return StatusResult.from(fetchCredentialDefinitions(process)) - .compose(credentialDefinitions -> generateCredential(process, credentialDefinitions)); + .compose(credentialDefinitions -> generateCredential(process, credentialDefinitions)) + .onSuccess(creds -> observable.invokeForEach(l -> l.generated(process, creds))); } private StatusResult> generateCredential(IssuanceProcess process, Collection credentialDefinitions) { @@ -181,6 +188,7 @@ private void transitionToError(IssuanceProcess process) { private void transitionToError(IssuanceProcess process, Throwable throwable) { transitionToError(process, throwable.getMessage()); + observable.invokeForEach(l -> l.errored(process, throwable)); } private void transitionToError(IssuanceProcess process, String message) { @@ -238,6 +246,11 @@ public Builder credentialStatusService(CredentialStatusService credentialStatusS return this; } + public Builder observable(IssuanceObservable observable) { + manager.observable = observable; + return this; + } + @Override public Builder self() { return this; @@ -251,6 +264,7 @@ public IssuanceProcessManagerImpl build() { Objects.requireNonNull(this.manager.credentialStore, "Credential store"); Objects.requireNonNull(this.manager.credentialStorageClient, "Credential service client"); Objects.requireNonNull(this.manager.credentialStatusService, "Credential status service"); + Objects.requireNonNull(this.manager.observable, "IssuanceObservable"); return manager; } } diff --git a/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImplTest.java b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImplTest.java index fef4298f9..4c463b402 100644 --- a/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImplTest.java +++ b/core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImplTest.java @@ -21,9 +21,12 @@ import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; import org.eclipse.edc.identityhub.spi.verifiablecredentials.store.CredentialStore; +import org.eclipse.edc.issuerservice.issuance.events.IssuanceObservableImpl; import org.eclipse.edc.issuerservice.spi.credentials.CredentialStatusService; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore; import org.eclipse.edc.issuerservice.spi.issuance.delivery.CredentialStorageClient; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEventListener; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGenerationRequest; import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGeneratorRegistry; import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition; @@ -59,6 +62,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -73,10 +77,13 @@ public class IssuanceProcessManagerImplTest { private final CredentialStore credentialStore = mock(); private final CredentialStorageClient credentialStorageClient = mock(); private final CredentialStatusService credentialStatusService = mock(); + private final IssuanceObservable issuanceObservable = new IssuanceObservableImpl(); + private final IssuanceEventListener listener = mock(); private IssuanceProcessManager issuanceProcessManager; @BeforeEach void setup() { + issuanceObservable.registerListener(listener); var entityRetryProcessConfiguration = new EntityRetryProcessConfiguration(1, () -> new ExponentialWaitStrategy(0L)); issuanceProcessManager = IssuanceProcessManagerImpl.Builder.newInstance() .entityRetryProcessConfiguration(entityRetryProcessConfiguration) @@ -87,6 +94,7 @@ void setup() { .credentialStore(credentialStore) .credentialStorageClient(credentialStorageClient) .credentialStatusService(credentialStatusService) + .observable(issuanceObservable) .monitor(monitor) .clock(clock) .build(); @@ -128,6 +136,7 @@ void approved_shouldGenerateAndDispatchCredentials() { when(credentialDefinitionStore.query(any())).thenReturn(StoreResult.success(List.of(credentialDefinition))); when(credentialGenerator.generateCredentials("participantContextId", "holderId", List.of(generationRequests), process.getClaims())).thenReturn(Result.success(List.of(credential))); when(credentialStore.create(any())).thenReturn(StoreResult.success()); + when(issuanceProcessStore.save(any())).thenReturn(StoreResult.success()); when(credentialStorageClient.deliverCredentials(process, List.of(credential))).thenReturn(Result.success()); when(credentialStatusService.addCredential(any(), any())).thenReturn(ServiceResult.success(credential.credential())); when(credentialGenerator.signCredential(any(), any(), any())).thenReturn(Result.success(credential)); @@ -149,6 +158,10 @@ void approved_shouldGenerateAndDispatchCredentials() { assertThat(cred.getVerifiableCredential().credential()).isEqualTo(credential.credential()); verify(issuanceProcessStore).save(argThat(p -> p.getState() == DELIVERED.code())); + + verify(listener).approved(process); + verify(listener).generated(eq(process), any()); + verify(listener).delivered(eq(process), any()); }); } @@ -180,10 +193,11 @@ void approved_shouldTransitionToErrored_whenGenerationErrors() { await().untilAsserted(() -> { verify(issuanceProcessStore).save(argThat(p -> p.getState() == ERRORED.code())); + verify(listener).approved(process); }); } private Criterion[] stateIs(int state) { - return aryEq(new Criterion[]{hasState(state), isNotPending()}); + return aryEq(new Criterion[]{ hasState(state), isNotPending() }); } } diff --git a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialApiEndToEndTest.java b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialApiEndToEndTest.java index e2f413067..ed769e8d7 100644 --- a/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialApiEndToEndTest.java +++ b/e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/CredentialApiEndToEndTest.java @@ -108,6 +108,7 @@ public class CredentialApiEndToEndTest { .issuanceDate(Instant.now()) .id(credentialId) .type("VerifiableCredential") + .type("TestCredential") .credentialSubject(CredentialSubject.Builder.newInstance().id(UUID.randomUUID().toString()).claim("foo", "bar").build()) .credentialStatus(new CredentialStatus(credentialId + "#status", "BitstringStatusListEntry", Map.of( "statusListIndex", STATUS_LIST_INDEX, @@ -314,6 +315,34 @@ void queryCredentials(IssuerService issuer, CredentialStore credentialStore) { } + @Test + void queryCredentials_byTypeAndParticipant(IssuerService issuer, CredentialStore credentialStore) { + + var credential1 = createCredential("test-cred"); + var credential2 = createCredential("test-cred-1", "another-user"); + var participantContextId = credential1.getHolderId(); + var type = "TestCredential"; + + credentialStore.create(credential1); + credentialStore.create(credential2); + + + issuer.getAdminEndpoint() + .baseRequest() + .contentType(JSON) + .header(authorizeUser(USER, issuer)) + .body(QuerySpec.Builder.newInstance() + .filter(new Criterion("holderId", "=", participantContextId)) + .filter(new Criterion("verifiableCredential.credential.type", "contains", type)) + .build()) + .post("/v1alpha/participants/%s/credentials/query".formatted(USER)) + .then() + .log().ifValidationFails() + .statusCode(200) + .body("size()", equalTo(1)); + + } + @Test void queryCredentials_notAuthorized(IssuerService issuer, CredentialStore credentialStore) { issuer.createParticipant(USER); diff --git a/e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/flow/DcpIssuanceFlowEndToEndTest.java b/e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/flow/DcpIssuanceFlowEndToEndTest.java index 4e8a4afb6..032ef7eac 100644 --- a/e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/flow/DcpIssuanceFlowEndToEndTest.java +++ b/e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/flow/DcpIssuanceFlowEndToEndTest.java @@ -29,6 +29,11 @@ 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.events.CredentialDelivered; +import org.eclipse.edc.issuerservice.spi.issuance.events.CredentialGenerated; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceApproved; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEvent; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceRequested; 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; @@ -38,6 +43,7 @@ import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.junit.extensions.ComponentRuntimeExtension; import org.eclipse.edc.junit.extensions.RuntimeExtension; +import org.eclipse.edc.spi.event.EventSubscriber; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndExtension; import org.eclipse.edc.validator.spi.ValidationResult; @@ -68,7 +74,9 @@ import static org.eclipse.edc.identityhub.tests.dcp.TestData.IH_RUNTIME_NAME; import static org.eclipse.edc.identityhub.tests.dcp.TestData.ISSUER_RUNTIME_NAME; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -108,6 +116,9 @@ static void beforeAll(IssuerService issuer, IdentityHub credentialService) { @ArgumentsSource(CredentialFormatProvider.class) void issuanceFlow(CredentialFormat format, String credentialType, IssuerService issuer, IdentityHub identityHub) { + var subscriber = mock(EventSubscriber.class); + issuer.registerListener(IssuanceEvent.class, subscriber); + var nameMapping = new MappingDefinition("participant.name", "credentialSubject.name", true); var idMapping = new MappingDefinition("participant.id", "credentialSubject.id", true); var credentialDefinitionId = UUID.randomUUID().toString(); @@ -199,6 +210,13 @@ void issuanceFlow(CredentialFormat format, String credentialType, IssuerService .header("Content-Type", "application/vc+jwt") .body(Matchers.notNullValue()); }); + + + var inOrder = inOrder(subscriber); + inOrder.verify(subscriber).on(argThat(env -> env.getPayload() instanceof IssuanceRequested)); + inOrder.verify(subscriber).on(argThat(env -> env.getPayload() instanceof IssuanceApproved)); + inOrder.verify(subscriber).on(argThat(env -> env.getPayload() instanceof CredentialGenerated)); + inOrder.verify(subscriber).on(argThat(env -> env.getPayload() instanceof CredentialDelivered)); } /** diff --git a/e2e-tests/identityhub-test-fixtures/src/testFixtures/java/org/eclipse/edc/identityhub/tests/fixtures/issuerservice/IssuerService.java b/e2e-tests/identityhub-test-fixtures/src/testFixtures/java/org/eclipse/edc/identityhub/tests/fixtures/issuerservice/IssuerService.java index 44344db2e..ce46bfef9 100644 --- a/e2e-tests/identityhub-test-fixtures/src/testFixtures/java/org/eclipse/edc/identityhub/tests/fixtures/issuerservice/IssuerService.java +++ b/e2e-tests/identityhub-test-fixtures/src/testFixtures/java/org/eclipse/edc/identityhub/tests/fixtures/issuerservice/IssuerService.java @@ -24,12 +24,15 @@ import org.eclipse.edc.issuerservice.spi.holder.model.Holder; import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionService; import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceEvent; 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.IssuanceProcess; import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; import org.eclipse.edc.junit.extensions.ComponentRuntimeContext; import org.eclipse.edc.junit.utils.LazySupplier; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.event.EventSubscriber; import org.eclipse.edc.spi.query.QuerySpec; import org.jetbrains.annotations.NotNull; @@ -142,6 +145,11 @@ public VerifiableCredentialResource getStatusListCredential() { .iterator().next(); } + public void registerListener(Class issuanceEventClass, EventSubscriber listener) { + var eventRouter = getService(EventRouter.class); + eventRouter.registerSync(issuanceEventClass, listener); + } + private @NotNull String issuanceBasePath(String participantContextId) { return "v1alpha/participants/%s".formatted((participantContextId)); } @@ -165,6 +173,13 @@ public Builder forContext(ComponentRuntimeContext ctx) { .issuerApiEndpoint(ctx.getEndpoint(ISSUANCE_API)); } + @Override + public IssuerService build() { + Objects.requireNonNull(instance.issuerApiEndpoint, "issuerApiEndpoint must be set"); + Objects.requireNonNull(instance.holderService, "holderService must be set"); + return super.build(); + } + public Builder issuerApiEndpoint(LazySupplier issuerApiEndpoint) { instance.issuerApiEndpoint = new LazySupplier<>(() -> new Endpoint(issuerApiEndpoint.get(), Map.of())); return this; @@ -189,12 +204,5 @@ public Builder credentialDefinitionService(CredentialDefinitionService service) instance.credentialDefinitionService = service; return this; } - - @Override - public IssuerService build() { - Objects.requireNonNull(instance.issuerApiEndpoint, "issuerApiEndpoint must be set"); - Objects.requireNonNull(instance.holderService, "holderService must be set"); - return super.build(); - } } } diff --git a/extensions/api/identity-api/identity-api-configuration/src/main/resources/identity-api-version.json b/extensions/api/identity-api/identity-api-configuration/src/main/resources/identity-api-version.json index 7a2e538fc..73b5ee132 100644 --- a/extensions/api/identity-api/identity-api-configuration/src/main/resources/identity-api-version.json +++ b/extensions/api/identity-api/identity-api-configuration/src/main/resources/identity-api-version.json @@ -2,7 +2,7 @@ { "version": "1.0.0-alpha", "urlPath": "/v1alpha", - "lastUpdated": "2026-03-30T11:00:00Z", + "lastUpdated": "2026-05-18T11:00:00Z", "maturity": null } ] diff --git a/extensions/api/issuer-admin-api/credentials-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentials/v1/unstable/IssuerCredentialsAdminApiController.java b/extensions/api/issuer-admin-api/credentials-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentials/v1/unstable/IssuerCredentialsAdminApiController.java index 266525785..a0fc3a3b5 100644 --- a/extensions/api/issuer-admin-api/credentials-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentials/v1/unstable/IssuerCredentialsAdminApiController.java +++ b/extensions/api/issuer-admin-api/credentials-api/src/main/java/org/eclipse/edc/issuerservice/api/admin/credentials/v1/unstable/IssuerCredentialsAdminApiController.java @@ -64,15 +64,14 @@ public IssuerCredentialsAdminApiController(AuthorizationService authorizationSer @POST @Path("/query") @RequiredScope("issuer-admin-api:read") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN }) @Override public Collection queryCredentials(@PathParam("participantContextId") String participantContextId, QuerySpec query, @Context SecurityContext context) { - var decodedParticipantContextId = participantContextId; - var spec = query.toBuilder().filter(filterByParticipantContextId(decodedParticipantContextId)).build(); + var spec = query.toBuilder().filter(filterByParticipantContextId(participantContextId)).build(); return credentialStatusService.queryCredentials(spec) .map(resources -> resources.stream() .filter(resource -> authorizationService - .authorize(context, decodedParticipantContextId, resource.getId(), VerifiableCredentialResource.class) + .authorize(context, participantContextId, resource.getId(), VerifiableCredentialResource.class) .succeeded()) .map(this::toDto) .toList()) @@ -81,7 +80,7 @@ public Collection queryCredentials(@PathParam(" @POST @RequiredScope("issuer-admin-api:write") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN }) @Path("/{credentialId}/revoke") @Override public void revokeCredential(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String credentialId, @Context SecurityContext context) { @@ -92,7 +91,7 @@ public void revokeCredential(@PathParam("participantContextId") String participa @POST @RequiredScope("issuer-admin-api:write") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN }) @Path("/{credentialId}/suspend") @Override public Response suspendCredential(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String credentialId) { @@ -101,7 +100,7 @@ public Response suspendCredential(@PathParam("participantContextId") String part @POST @RequiredScope("issuer-admin-api:write") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN }) @Path("/{credentialId}/resume") @Override public Response resumeCredential(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String credentialId) { @@ -110,7 +109,7 @@ public Response resumeCredential(@PathParam("participantContextId") String parti @GET @RequiredScope("issuer-admin-api:read") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PROVISIONER}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PROVISIONER }) @Path("/{credentialId}/status") @Override public CredentialStatusResponse checkRevocationStatus(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String credentialId, @Context SecurityContext context) { @@ -122,7 +121,7 @@ public CredentialStatusResponse checkRevocationStatus(@PathParam("participantCon @POST @RequiredScope("issuer-admin-api:write") - @RolesAllowed({ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN}) + @RolesAllowed({ ParticipantPrincipal.ROLE_PARTICIPANT, ParticipantPrincipal.ROLE_ADMIN }) @Path("/offer") @Override public void sendCredentialOffer(@PathParam("participantContextId") String participantContextId, CredentialOfferDto credentialOffer, @Context SecurityContext context) { diff --git a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java index 3cd83491e..aaa6a1fbe 100644 --- a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java +++ b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerCoreExtension.java @@ -27,6 +27,7 @@ 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.delivery.CredentialStorageClient; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore; import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator; import org.eclipse.edc.jwt.validation.jti.JtiValidationStore; @@ -120,6 +121,8 @@ public class DcpIssuerCoreExtension implements ServiceExtension { private boolean allowAnonymousCredentialRequest; @Inject private Telemetry telemetry; + @Inject + private IssuanceObservable issuanceObservable; @Override public void initialize(ServiceExtensionContext context) { @@ -138,7 +141,7 @@ public void initialize(ServiceExtensionContext context) { @Provider public DcpIssuerService createIssuerService() { - return new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator, profileRegistry, telemetry); + return new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator, profileRegistry, telemetry, issuanceObservable); } @Provider diff --git a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java index ab068c8fd..08297d8a9 100644 --- a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java +++ b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/main/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImpl.java @@ -23,6 +23,7 @@ import org.eclipse.edc.identityhub.protocols.dcp.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.events.IssuanceObservable; 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; @@ -48,13 +49,14 @@ public class DcpIssuerServiceImpl implements DcpIssuerService { private final CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator; private final DcpProfileRegistry profileRegistry; private final Telemetry telemetry; + private final IssuanceObservable observable; public DcpIssuerServiceImpl(TransactionContext transactionContext, CredentialDefinitionService credentialDefinitionService, IssuanceProcessStore issuanceProcessStore, AttestationPipeline attestationPipeline, CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator, - DcpProfileRegistry profileRegistry, Telemetry telemetry) { + DcpProfileRegistry profileRegistry, Telemetry telemetry, IssuanceObservable observable) { this.transactionContext = transactionContext; this.credentialDefinitionService = credentialDefinitionService; this.issuanceProcessStore = issuanceProcessStore; @@ -62,6 +64,7 @@ public DcpIssuerServiceImpl(TransactionContext transactionContext, this.credentialRuleDefinitionEvaluator = credentialRuleDefinitionEvaluator; this.profileRegistry = profileRegistry; this.telemetry = telemetry; + this.observable = observable; } @WithSpan(value = "issuance.initiate") @@ -80,6 +83,12 @@ public ServiceResult initiateCredentialsIssua .compose(credentialDefinitions -> evaluateAttestations(context, credentialDefinitions)) .compose(this::evaluateRules) .compose(evaluation -> createIssuanceProcess(participantContextId, message.getHolderPid(), credentialFormats.getContent(), context, evaluation)) + .onSuccess(ip -> { + observable.invokeForEach(l -> l.received(ip)); + }) + .onFailure(f -> { + observable.invokeForEach(l -> l.rejected(message.getHolderPid(), participantContextId, f.getFailureDetail())); + }) .map(issuanceProcess -> new CredentialRequestMessage.Response(issuanceProcess.getId()))); } diff --git a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java index 9dbb5da80..430b85538 100644 --- a/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java +++ b/protocols/dcp/dcp-issuer/dcp-issuer-core/src/test/java/org/eclipse/edc/identityhub/protocols/dcp/issuer/DcpIssuerServiceImplTest.java @@ -23,6 +23,8 @@ import org.eclipse.edc.issuerservice.spi.holder.model.Holder; 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.events.IssuanceEventListener; +import org.eclipse.edc.issuerservice.spi.issuance.events.IssuanceObservable; 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; @@ -40,6 +42,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat.VC1_0_JWT; @@ -48,6 +51,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -59,8 +63,10 @@ public class DcpIssuerServiceImplTest { private final IssuanceProcessStore issuanceProcessStore = mock(); private final CredentialRuleDefinitionEvaluator credentialRuleDefinitionEvaluator = mock(); private final DcpProfileRegistry dcpProfileRegistry = mock(); + private final IssuanceObservable issuanceObservable = mock(); - private final DcpIssuerService dcpIssuerService = new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator, dcpProfileRegistry, mock()); + private final DcpIssuerService dcpIssuerService = new DcpIssuerServiceImpl(transactionContext, credentialDefinitionService, + issuanceProcessStore, attestationPipeline, credentialRuleDefinitionEvaluator, dcpProfileRegistry, mock(), issuanceObservable); @Test @@ -113,6 +119,58 @@ void initiateCredentialsIssuance() { assertThat(issuanceProcess.getClaims()).containsAllEntriesOf(claims); assertThat(issuanceProcess.getParticipantContextId()).isEqualTo("participantContextId"); assertThat(issuanceProcess.getHolderPid()).isEqualTo(message.getHolderPid()); + + var listenerCaptor = ArgumentCaptor.forClass(Consumer.class); + //noinspection unchecked + verify(issuanceObservable).invokeForEach(listenerCaptor.capture()); + var listener = mock(IssuanceEventListener.class); + //noinspection unchecked + listenerCaptor.getValue().accept(listener); + verify(listener).received(issuanceProcess); + } + + @Test + void initiateCredentialsIssuance_failure() { + + var message = CredentialRequestMessage.Builder.newInstance() + .holderPid(UUID.randomUUID().toString()) + .credential(new CredentialRequestSpecifier("credentialDefinitionId1")) + .build(); + + var attestations = Set.of("attestation1", "attestation2"); + + var credentialRuleDefinition = new CredentialRuleDefinition("expression", Map.of()); + var credentialDefinition = CredentialDefinition.Builder.newInstance() + .id("credentialDefinitionId1") + .credentialType("MembershipCredential") + .jsonSchema("jsonSchema") + .jsonSchemaUrl("jsonSchemaUrl") + .attestations(attestations) + .participantContextId("participantContextId") + .rule(credentialRuleDefinition) + .formatFrom(VC1_0_JWT) + .build(); + + var holder = Holder.Builder.newInstance().holderId("holderId").did("participantDid").holderName("name").participantContextId("participantContextId").build(); + var participant = new DcpRequestContext(holder, Map.of()); + + when(credentialDefinitionService.queryCredentialDefinitions(any())).thenReturn(ServiceResult.notFound("test-failure")); + when(credentialDefinitionService.findCredentialDefinitionById(anyString())).thenReturn(ServiceResult.success(credentialDefinition)); + when(attestationPipeline.evaluate(eq(attestations), any())).thenReturn(Result.failure("test-failure")); + + var result = dcpIssuerService.initiateCredentialsIssuance("participantContextId", message, participant); + + assertThat(result).isFailed(); + + verify(issuanceProcessStore, never()).save(any()); + + var listenerCaptor = ArgumentCaptor.forClass(Consumer.class); + //noinspection unchecked + verify(issuanceObservable).invokeForEach(listenerCaptor.capture()); + var listener = mock(IssuanceEventListener.class); + //noinspection unchecked + listenerCaptor.getValue().accept(listener); + verify(listener).rejected(eq(message.getHolderPid()), eq("participantContextId"), eq("test-failure")); } } diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialDelivered.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialDelivered.java new file mode 100644 index 000000000..0535cf05f --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialDelivered.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; + +import java.util.Collection; +import java.util.List; + +public class CredentialDelivered extends IssuanceEvent { + private Collection credentials; + + public Collection getCredentials() { + return credentials; + } + + @Override + public String name() { + return "issuance.credential.delivered"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new CredentialDelivered()); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder credentials(List credentials) { + event.credentials = credentials; + return self(); + } + + @Override + public Builder self() { + return this; + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialGenerated.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialGenerated.java new file mode 100644 index 000000000..ceef47375 --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/CredentialGenerated.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; + +import java.util.Collection; +import java.util.List; + +public class CredentialGenerated extends IssuanceEvent { + private Collection credentials; + + public Collection getCredentials() { + return credentials; + } + + @Override + public String name() { + return "issuance.credential.generated"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new CredentialGenerated()); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder credentials(List credentials) { + event.credentials = credentials; + return self(); + } + + @Override + public Builder self() { + return this; + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceApproved.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceApproved.java new file mode 100644 index 000000000..dbb6916c0 --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceApproved.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +public class IssuanceApproved extends IssuanceEvent { + @Override + public String name() { + return "issuance.approved"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new IssuanceApproved()); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + @Override + public Builder self() { + return this; + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEvent.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEvent.java new file mode 100644 index 000000000..f5c0ac04b --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEvent.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + + +import org.eclipse.edc.spi.event.Event; + +import java.util.Objects; + +public abstract class IssuanceEvent extends Event { + + protected String holderId; + protected String issuerParticipantContextId; + protected String holderProcessId; + protected String issuanceProcessId; + + /** + * The ID of the holder that requested the credential. + */ + public String getHolderId() { + return holderId; + } + + /** + * Retrieves the ID of the issuer participant context. + */ + public String getIssuerParticipantContextId() { + return issuerParticipantContextId; + } + + /** + * Retrieves the ID that the holder has assigned to the issuance process. + */ + public String getHolderProcessId() { + return holderProcessId; + } + + /** + * Retrieves the ID of the issuance process. + */ + public String getIssuanceProcessId() { + return issuanceProcessId; + } + + public abstract static class Builder> { + protected T event; + + protected Builder(T event) { + this.event = event; + } + + public abstract B self(); + + public B holderId(String holderId) { + event.holderId = holderId; + return self(); + } + + public B issuerParticipantContextId(String issuerParticipantContextId) { + event.issuerParticipantContextId = issuerParticipantContextId; + return self(); + } + + public B holderProcessId(String holderProcessId) { + event.holderProcessId = holderProcessId; + return self(); + } + + public T build() { + Objects.requireNonNull(event.issuerParticipantContextId); + Objects.requireNonNull(event.holderId); + return event; + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEventListener.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEventListener.java new file mode 100644 index 000000000..b2fc8b25e --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceEventListener.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcess; +import org.eclipse.edc.spi.observe.Observable; + +import java.util.Collection; + +/** + * Interface implemented by listeners registered to observe credential issuance changes via {@link Observable#registerListener}. + * The listener must be called after the state changes are persisted. + */ +public interface IssuanceEventListener { + + /** + * A credential issuance request was successfully received and will be processed further by the Issuer service. + * An {@link IssuanceApproved} has been created to represent the request internally. + */ + default void received(IssuanceProcess ip) { + + } + + /** + * A credential issuance request was received but rejected by the Issuer service. No {@link IssuanceRejected} has been created. + */ + default void rejected(String holderPid, String issuerParticipantContextId, String failureDetail) { + + } + + /** + * The Issuer service approved a credential issuance request. + */ + default void approved(IssuanceProcess process) { + + } + + /** + * The credentials requested in the issuance request have been generated (signed). + */ + default void generated(IssuanceProcess process, Collection creds) { + + } + + /** + * The credentials requested in the issuance request have been delivered successfully to the holder. + */ + default void delivered(IssuanceProcess process, Collection credentials) { + + } + + /** + * A credential issuance request failed. The associated {@link IssuanceProcess} is in the {@link org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcessStates#ERRORED} + * state. + */ + default void errored(IssuanceProcess process, Throwable throwable) { + + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceObservable.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceObservable.java new file mode 100644 index 000000000..5c84583b2 --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceObservable.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import org.eclipse.edc.spi.observe.Observable; + +public interface IssuanceObservable extends Observable { +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceProcessErrored.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceProcessErrored.java new file mode 100644 index 000000000..d66a6adf3 --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceProcessErrored.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +public class IssuanceProcessErrored extends IssuanceEvent { + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public String name() { + return "issuance.errored"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new IssuanceProcessErrored()); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder errorMessage(String errorMessage) { + event.errorMessage = errorMessage; + return self(); + } + + @Override + public Builder self() { + return this; + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRejected.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRejected.java new file mode 100644 index 000000000..3f7a9bc96 --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRejected.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +public class IssuanceRejected extends IssuanceEvent { + private String reason; + + public String getReason() { + return reason; + } + + @Override + public String name() { + return "issuance.rejected"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new IssuanceRejected()); + } + + @JsonCreator + public static IssuanceRejected.Builder newInstance() { + return new IssuanceRejected.Builder(); + } + + + @Override + public IssuanceRejected.Builder self() { + return this; + } + + public Builder reason(String failureDetail) { + event.reason = failureDetail; + return self(); + } + } +} diff --git a/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRequested.java b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRequested.java new file mode 100644 index 000000000..213d2751d --- /dev/null +++ b/spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/events/IssuanceRequested.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.issuerservice.spi.issuance.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; + +import java.util.List; +import java.util.Map; + +public class IssuanceRequested extends IssuanceEvent { + private List credentialDefinitionIds; + private Map credentialFormats; + + public List getCredentialDefinitionIds() { + return credentialDefinitionIds; + } + + public Map getCredentialFormats() { + return credentialFormats; + } + + @Override + public String name() { + return "issuance.requested"; + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends IssuanceEvent.Builder { + + protected Builder() { + super(new IssuanceRequested()); + } + + @JsonCreator + public static Builder newInstance() { + return new IssuanceRequested.Builder(); + } + + public Builder credentialDefinitionIds(List credentialDefinitionIds) { + event.credentialDefinitionIds = credentialDefinitionIds; + return this; + } + + public Builder credentialFormats(Map credentialFormats) { + event.credentialFormats = credentialFormats; + return this; + } + + + @Override + public Builder self() { + return this; + } + } +}