Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -80,6 +84,9 @@ public class IssuanceServicesExtension implements ServiceExtension {
@Inject
private IdentityHubParticipantContextService participantContextService;

@Inject
private EventRouter eventRouter;

private AttestationPipelineImpl attestationPipeline;

private CredentialRuleFactoryRegistry ruleFactoryRegistry;
Expand All @@ -89,6 +96,7 @@ public class IssuanceServicesExtension implements ServiceExtension {
private AttestationDefinitionValidatorRegistry attestationDefinitionValidatorRegistry;

private IssuanceClaimsMapper issuanceClaimsMapper;
private IssuanceObservable issuanceObservable;

@Provider
public CredentialDefinitionService createParticipantService() {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VerifiableCredentialContainer> 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<VerifiableCredentialContainer> 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 <E extends IssuanceEvent, B extends IssuanceEvent.Builder<E, B>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<IssuanceEventListener> implements IssuanceObservable {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +54,7 @@

public class IssuanceProcessManagerImpl extends AbstractStateEntityManager<IssuanceProcess, IssuanceProcessStore> implements IssuanceProcessManager {

private IssuanceObservable observable;
private CredentialGeneratorRegistry credentialGenerator;
private CredentialDefinitionStore credentialDefinitionStore;
private CredentialStore credentialStore;
Expand All @@ -73,12 +75,16 @@ protected StateMachineManager.Builder configureStateMachineManager(StateMachineM
*/
@WithSpan(value = "issuance.approved")
private CompletableFuture<StatusResult<Void>> 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();
Expand Down Expand Up @@ -108,7 +114,8 @@ private StatusResult<Collection<VerifiableCredentialContainer>> addCredentialsTo

private StatusResult<Collection<VerifiableCredentialContainer>> 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<Collection<VerifiableCredentialContainer>> generateCredential(IssuanceProcess process, Collection<CredentialDefinition> credentialDefinitions) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -87,6 +94,7 @@ void setup() {
.credentialStore(credentialStore)
.credentialStorageClient(credentialStorageClient)
.credentialStatusService(credentialStatusService)
.observable(issuanceObservable)
.monitor(monitor)
.clock(clock)
.build();
Expand Down Expand Up @@ -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));
Expand All @@ -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());
});
}

Expand Down Expand Up @@ -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() });
}
}
Loading
Loading