Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.edc.issuerservice.spi.issuance.delivery.CredentialStorageClient;
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.IssuanceProcessPendingGuard;
import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessService;
import org.eclipse.edc.issuerservice.spi.issuance.process.retry.IssuanceProcessRetryStrategy;
import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore;
Expand Down Expand Up @@ -87,6 +88,9 @@ public class IssuanceCoreExtension implements ServiceExtension {
@Inject
private CredentialStatusService credentialStatusService;

@Inject
IssuanceProcessPendingGuard issuanceProcessPendingGuard;

@Provider
public IssuanceProcessManager createIssuanceProcessManager() {

Expand All @@ -106,6 +110,7 @@ public IssuanceProcessManager createIssuanceProcessManager() {
.credentialStorageClient(credentialStorageClient)
.credentialStatusService(credentialStatusService)
.entityRetryProcessConfiguration(stateMachineConfiguration.entityRetryProcessConfiguration())
.pendingGuard(issuanceProcessPendingGuard)
.build();
}
return issuanceProcessManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore;
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.process.IssuanceProcessPendingGuard;
import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionEvaluator;
import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleDefinitionValidatorRegistry;
import org.eclipse.edc.issuerservice.spi.issuance.rule.CredentialRuleFactoryRegistry;
Expand Down Expand Up @@ -157,6 +158,11 @@ public AttestationDefinitionValidatorRegistry createAttestationDefinitionValidat
return attestationDefinitionValidatorRegistry;
}

@Provider(isDefault = true)
public IssuanceProcessPendingGuard issuanceProcessPendingGuard() {
return issuanceProcess -> false;
}

private AttestationPipelineImpl createAttestationPipelineImpl() {
if (attestationPipeline == null) {
attestationPipeline = new AttestationPipelineImpl(attestationDefinitionStore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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.IssuanceProcessManager;
import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessPendingGuard;
import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
Expand Down Expand Up @@ -57,6 +58,7 @@ public class IssuanceProcessManagerImpl extends AbstractStateEntityManager<Issua
private CredentialStore credentialStore;
private CredentialStorageClient credentialStorageClient;
private CredentialStatusService credentialStatusService;
private IssuanceProcessPendingGuard issuanceProcessPendingGuard = ip -> false;

private IssuanceProcessManagerImpl() {
}
Expand Down Expand Up @@ -194,10 +196,18 @@ private Processor processIssuanceInState(IssuanceProcessStates state, Function<I
private ProcessorImpl<IssuanceProcess> createProcessor(Function<IssuanceProcess, Boolean> function, Criterion[] filter) {
return ProcessorImpl.Builder.newInstance(() -> store.nextNotLeased(batchSize, filter))
.process(telemetry.contextPropagationMiddleware(function))
.guard(issuanceProcessPendingGuard, this::setPending)
.onNotProcessed(this::breakLease)
.build();
}

private boolean setPending(IssuanceProcess issuanceProcess) {
issuanceProcess.setPending(true);
update(issuanceProcess);
return true;
}


public static class Builder
extends AbstractStateEntityManager.Builder<IssuanceProcess, IssuanceProcessStore, IssuanceProcessManagerImpl, Builder> {

Expand Down Expand Up @@ -235,6 +245,12 @@ public Builder credentialStatusService(CredentialStatusService credentialStatusS
return this;
}

public Builder pendingGuard(IssuanceProcessPendingGuard issuanceProcessPendingGuard) {
manager.issuanceProcessPendingGuard = issuanceProcessPendingGuard;
return this;
}


@Override
public Builder self() {
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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.IssuanceProcessManager;
import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessPendingGuard;
import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.query.Criterion;
Expand Down Expand Up @@ -59,6 +60,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -73,6 +75,7 @@ public class IssuanceProcessManagerImplTest {
private final CredentialStore credentialStore = mock();
private final CredentialStorageClient credentialStorageClient = mock();
private final CredentialStatusService credentialStatusService = mock();
private final IssuanceProcessPendingGuard pendingGuard = mock();
private IssuanceProcessManager issuanceProcessManager;

@BeforeEach
Expand All @@ -87,6 +90,7 @@ void setup() {
.credentialStore(credentialStore)
.credentialStorageClient(credentialStorageClient)
.credentialStatusService(credentialStatusService)
.pendingGuard(pendingGuard)
.monitor(monitor)
.clock(clock)
.build();
Expand Down Expand Up @@ -152,6 +156,30 @@ void approved_shouldGenerateAndDispatchCredentials() {
});
}

@Test
void pendingGuard_shouldSetPending_whenGuardMatches() {
when(pendingGuard.test(any())).thenReturn(true);
var process = IssuanceProcess.Builder.newInstance()
.state(APPROVED.code())
.holderId("holderId")
.participantContextId("participantContextId")
.holderPid("holderPid")
.build();
when(issuanceProcessStore.nextNotLeased(anyInt(), stateIs(APPROVED.code()))).thenReturn(List.of(process)).thenReturn(emptyList());

issuanceProcessManager.start();

await().untilAsserted(() -> {
var captor = ArgumentCaptor.forClass(IssuanceProcess.class);
verify(issuanceProcessStore, atLeastOnce()).save(captor.capture());
var saved = captor.getValue();
assertThat(saved.getState()).isEqualTo(APPROVED.code());
assertThat(saved.isPending()).isTrue();
});

verify(pendingGuard).test(process);
}

@Test
void approved_shouldTransitionToErrored_whenGenerationErrors() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition;
import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcessStates;
import org.eclipse.edc.issuerservice.spi.issuance.model.MappingDefinition;
import org.eclipse.edc.issuerservice.spi.issuance.process.IssuanceProcessPendingGuard;
import org.eclipse.edc.junit.annotations.EndToEndTest;
import org.eclipse.edc.junit.extensions.ComponentRuntimeExtension;
import org.eclipse.edc.junit.extensions.RuntimeExtension;
Expand Down Expand Up @@ -74,6 +75,7 @@ public class DcpAnonymousIssuanceFlowEndToEndTest {


protected static final AttestationSourceFactory ATTESTATION_SOURCE_FACTORY = mock();
protected static final IssuanceProcessPendingGuard ISSUANCE_PROCESS_PENDING_GUARD = mock(IssuanceProcessPendingGuard.class);

protected static final Duration TIMEOUT = Duration.ofSeconds(60);
protected static final Duration INTERVAL = Duration.ofSeconds(1);
Expand Down Expand Up @@ -116,6 +118,7 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
when(ATTESTATION_SOURCE_FACTORY.createSource(refEq(attestationDefinition))).thenReturn(attestationSource);
when(attestationSource.execute(any()))
.thenReturn(Result.success(Map.of("onboarding", Map.of("signedDocuments", true), "participant", Map.of("name", "Alice"))));
when(ISSUANCE_PROCESS_PENDING_GUARD.test(any())).thenReturn(false);

var request = """
{
Expand All @@ -138,7 +141,7 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
// wait for the request status to be requested on the holder side
await().pollInterval(INTERVAL)
.atMost(TIMEOUT)
.untilAsserted(() -> assertThat(identityHub.getCredentialRequestForParticipant(PARTICIPANT_ID)).hasSize(1)
.untilAsserted(() -> assertThat(identityHub.getCredentialRequestForParticipant(PARTICIPANT_ID, "test-request-id")).hasSize(1)
.allSatisfy(t -> {
assertThat(t.getState()).isEqualTo(HolderRequestState.ISSUED.code());
assertThat(t.getHolderPid()).isEqualTo("test-request-id");
Expand All @@ -147,7 +150,7 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
// wait for the issuance process to be delivered on the issuer side
await().pollInterval(INTERVAL)
.atMost(TIMEOUT)
.untilAsserted(() -> assertThat(issuer.getIssuanceProcessesForParticipant(ISSUER_ID)).hasSize(1)
.untilAsserted(() -> assertThat(issuer.getIssuanceProcessesForParticipant(ISSUER_ID, "test-request-id")).hasSize(1)
.allSatisfy(t -> {
assertThat(t.getHolderPid()).isEqualTo("test-request-id");
assertThat(t.getState()).isEqualTo(IssuanceProcessStates.DELIVERED.code());
Expand Down Expand Up @@ -195,6 +198,49 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
});
}

@Test
void issuanceFlow_withGuard(IssuerService issuer, IdentityHub identityHub) {

var mappingDefinition = new MappingDefinition("participant.name", "credentialSubject.name", true);
var attestationDefinition = setupIssuer(issuer, Map.of(
"claim", "onboarding.signedDocuments",
"operator", "eq",
"value", true), mappingDefinition);

var attestationSource = mock(AttestationSource.class);
when(ATTESTATION_SOURCE_FACTORY.createSource(refEq(attestationDefinition))).thenReturn(attestationSource);
when(attestationSource.execute(any()))
.thenReturn(Result.success(Map.of("onboarding", Map.of("signedDocuments", true), "participant", Map.of("name", "Alice"))));
when(ISSUANCE_PROCESS_PENDING_GUARD.test(any())).thenReturn(true);

var request = """
{
"issuerDid": "%s",
"holderPid": "test-request-with-guard-id",
"credentials": [{ "format": "VC1_0_JWT", "id": "membershipCredential-id", "type": "MembershipCredential" }]
}
""".formatted(issuerDid);

identityHub.getIdentityEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", participantToken))
.body(request)
.post("/v1alpha/participants/%s/credentials/request".formatted(base64Encode(PARTICIPANT_ID)))
.then()
.log().ifValidationFails()
.statusCode(201)
.header("Location", Matchers.endsWith("/credentials/request/test-request-with-guard-id"));

// wait for the issuance process to be pending on the issuer side
await().pollInterval(INTERVAL)
.atMost(TIMEOUT)
.untilAsserted(() -> assertThat(issuer.getIssuanceProcessesForParticipant(ISSUER_ID, "test-request-with-guard-id")).hasSize(1)
.allSatisfy(t -> {
assertThat(t.isPending()).isTrue();
assertThat(t.getHolderPid()).isEqualTo("test-request-with-guard-id");
}));
}

/**
* Setup the issuer with an attestation definition and a credential definition
*/
Expand Down Expand Up @@ -253,7 +299,8 @@ class InMemory extends Tests {
.paramProvider(IssuerService.class, IssuerService::forContext)
.modules(DefaultRuntimes.Issuer.MODULES)
.configurationProvider(() -> ConfigFactory.fromMap(Map.of("edc.issuance.anonymous.allowed", "true")))
.build();
.build()
.registerServiceMock(IssuanceProcessPendingGuard.class, ISSUANCE_PROCESS_PENDING_GUARD);

}

Expand All @@ -275,7 +322,8 @@ class Postgres extends Tests {
.configurationProvider(DefaultRuntimes.Issuer::config)
.paramProvider(IssuerService.class, IssuerService::forContext)
.configurationProvider(() -> POSTGRESQL_EXTENSION.configFor(ISSUER).merge(ConfigFactory.fromMap(Map.of("edc.issuance.anonymous.allowed", "true"))))
.build();
.build()
.registerServiceMock(IssuanceProcessPendingGuard.class, ISSUANCE_PROCESS_PENDING_GUARD);
private static final String IDENTITY_HUB = "identityhub";

@Order(1) // must be the first extension to be evaluated since it starts the DB server
Expand Down
Loading
Loading