From f906ff941233ea60380146ce9c43fe2ae5e5b933 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:22:44 +0200 Subject: [PATCH] chore: align DID operations with DR (#450) * chore: align DID operations with DR * checkstyle * fix test, deactivate PC before delete --- core/identity-hub-did/build.gradle.kts | 1 + .../did/DidDocumentServiceImpl.java | 82 ++++++++++----- .../identityhub/did/DidServicesExtension.java | 6 +- .../did/DidDocumentServiceImplTest.java | 47 ++++++++- ...articipantContextCoordinatorExtension.java | 57 +++++++++++ .../ParticipantContextExtension.java | 18 ---- .../ParticipantContextServiceImpl.java | 24 +++-- ...rg.eclipse.edc.spi.system.ServiceExtension | 3 +- .../ParticipantContextServiceImplTest.java | 7 +- .../tests/DidManagementApiEndToEndTest.java | 99 +++++++++++++++---- .../tests/KeyPairResourceApiEndToEndTest.java | 16 ++- .../tests/PresentationApiEndToEndTest.java | 17 ++-- .../VerifiableCredentialApiEndToEndTest.java | 16 ++- 13 files changed, 299 insertions(+), 94 deletions(-) create mode 100644 core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextCoordinatorExtension.java diff --git a/core/identity-hub-did/build.gradle.kts b/core/identity-hub-did/build.gradle.kts index 956cf1a4c..d5b27799d 100644 --- a/core/identity-hub-did/build.gradle.kts +++ b/core/identity-hub-did/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { api(project(":spi:did-spi")) implementation(project(":spi:keypair-spi")) + implementation(project(":spi:identity-hub-store-spi")) implementation(project(":spi:participant-context-spi")) implementation(libs.edc.core.connector) // for the reflection-based query resolver implementation(libs.edc.lib.common.crypto) diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java index 6b22cd8b6..5fd53a96f 100644 --- a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java @@ -24,7 +24,10 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; import org.eclipse.edc.keys.spi.KeyParserRegistry; import org.eclipse.edc.security.token.jwt.CryptoConverter; @@ -54,13 +57,16 @@ public class DidDocumentServiceImpl implements DidDocumentService, EventSubscrib private final TransactionContext transactionContext; private final DidResourceStore didResourceStore; private final DidDocumentPublisherRegistry registry; + private final ParticipantContextService participantContextService; private final Monitor monitor; private final KeyParserRegistry keyParserRegistry; - public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry, Monitor monitor, KeyParserRegistry keyParserRegistry) { + public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry, + ParticipantContextService participantContextService, Monitor monitor, KeyParserRegistry keyParserRegistry) { this.transactionContext = transactionContext; this.didResourceStore = didResourceStore; this.registry = registry; + this.participantContextService = participantContextService; this.monitor = monitor; this.keyParserRegistry = keyParserRegistry; } @@ -101,18 +107,30 @@ public ServiceResult deleteById(String did) { @Override public ServiceResult publish(String did) { return transactionContext.execute(() -> { - var existingDoc = didResourceStore.findById(did); - if (existingDoc == null) { + var existingResource = didResourceStore.findById(did); + if (existingResource == null) { return ServiceResult.notFound(notFoundMessage(did)); } - var publisher = registry.getPublisher(did); - if (publisher == null) { - return ServiceResult.badRequest(noPublisherFoundMessage(did)); - } - var publishResult = publisher.publish(did); - return publishResult.succeeded() ? - success() : - ServiceResult.badRequest(publishResult.getFailureDetail()); + var participantId = existingResource.getParticipantId(); + return participantContextService.getParticipantContext(participantId) + .map(ParticipantContext::getStateAsEnum) + .compose(state -> { + var canPublish = state.equals(ParticipantContextState.ACTIVATED); + if (canPublish) { + var publisher = registry.getPublisher(did); + if (publisher == null) { + return ServiceResult.badRequest(noPublisherFoundMessage(did)); + } + var publishResult = publisher.publish(did); + return publishResult.succeeded() ? + success() : + ServiceResult.badRequest(publishResult.getFailureDetail()); + } + return ServiceResult.badRequest(("Cannot publish DID '%s' for participant '%s' because the ParticipantContext is not state '%s' state, " + + "but was '%s'.") + .formatted(did, participantId, ParticipantContextState.ACTIVATED, state)); + }); + }); } @@ -120,23 +138,35 @@ public ServiceResult publish(String did) { @Override public ServiceResult unpublish(String did) { return transactionContext.execute(() -> { - var existingDoc = didResourceStore.findById(did); - if (existingDoc == null) { + var existingResource = didResourceStore.findById(did); + if (existingResource == null) { return ServiceResult.notFound(notFoundMessage(did)); } - var publisher = registry.getPublisher(did); - if (publisher == null) { - return ServiceResult.badRequest(noPublisherFoundMessage(did)); - } - // only unpublish if published, NOOP otherwise - if (existingDoc.getState() == DidState.PUBLISHED.code()) { - var publishResult = publisher.unpublish(did); - return publishResult.succeeded() ? - success() : - ServiceResult.badRequest(publishResult.getFailureDetail()); - } - monitor.info("Unpublishing DID Document '%s': not in state '%s', unpublishing is a NOOP.".formatted(did, existingDoc.getStateAsEnum())); - return success(); + + var participantId = existingResource.getParticipantId(); + return participantContextService.getParticipantContext(participantId) + .map(ParticipantContext::getStateAsEnum) + .compose(state -> { + var canUnpublish = state.equals(ParticipantContextState.DEACTIVATED); + if (canUnpublish) { + var publisher = registry.getPublisher(did); + if (publisher == null) { + return ServiceResult.badRequest(noPublisherFoundMessage(did)); + } + // only unpublish if published, NOOP otherwise + if (existingResource.getState() == DidState.PUBLISHED.code()) { + var publishResult = publisher.unpublish(did); + return publishResult.succeeded() ? + success() : + ServiceResult.badRequest(publishResult.getFailureDetail()); + } + monitor.info("Unpublishing DID Document '%s': not in state '%s', unpublishing is a NOOP.".formatted(did, existingResource.getStateAsEnum())); + return success(); + } + return ServiceResult.badRequest(("Cannot un-publish DID '%s' for participant '%s' because the ParticipantContext is not state '%s' state, " + + "but was '%s'.") + .formatted(did, participantId, ParticipantContextState.DEACTIVATED, state)); + }); }); } diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidServicesExtension.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidServicesExtension.java index 92f108d70..211f2d5ed 100644 --- a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidServicesExtension.java +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidServicesExtension.java @@ -19,6 +19,7 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated; import org.eclipse.edc.keys.spi.KeyParserRegistry; import org.eclipse.edc.runtime.metamodel.annotation.Extension; @@ -46,6 +47,8 @@ public class DidServicesExtension implements ServiceExtension { @Inject private KeyParserRegistry keyParserRegistry; + @Inject + private ParticipantContextService participantContextService; @Override public String name() { @@ -62,7 +65,8 @@ public DidDocumentPublisherRegistry getDidPublisherRegistry() { @Provider public DidDocumentService createDidDocumentService(ServiceExtensionContext context) { - var service = new DidDocumentServiceImpl(transactionContext, didResourceStore, getDidPublisherRegistry(), context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry); + var service = new DidDocumentServiceImpl(transactionContext, didResourceStore, + getDidPublisherRegistry(), participantContextService, context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry); eventRouter.registerSync(ParticipantContextUpdated.class, service); eventRouter.registerSync(KeyPairAdded.class, service); eventRouter.registerSync(KeyPairRevoked.class, service); diff --git a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java index 32a0ce376..0e84d35b8 100644 --- a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java +++ b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java @@ -28,7 +28,9 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState; import org.eclipse.edc.keys.KeyParserRegistryImpl; import org.eclipse.edc.keys.keyparsers.JwkParser; @@ -38,6 +40,7 @@ import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.transaction.spi.NoopTransactionContext; import org.junit.jupiter.api.BeforeEach; @@ -60,9 +63,12 @@ import static org.mockito.Mockito.when; class DidDocumentServiceImplTest { + public static final String TEST_DID = "did:web:testdid"; + private static final String TEST_PARTICIPANT_ID = "test-participant"; private final DidResourceStore didResourceStoreMock = mock(); private final DidDocumentPublisherRegistry publisherRegistry = mock(); private final DidDocumentPublisher publisherMock = mock(); + private final ParticipantContextService participantContextServiceMock = mock(); private DidDocumentServiceImpl service; private Monitor monitorMock; @@ -75,7 +81,13 @@ void setUp() { registry.register(new JwkParser(new ObjectMapper(), mock())); registry.register(new PemParser(mock())); monitorMock = mock(); - service = new DidDocumentServiceImpl(trx, didResourceStoreMock, publisherRegistry, monitorMock, registry); + service = new DidDocumentServiceImpl(trx, didResourceStoreMock, publisherRegistry, participantContextServiceMock, monitorMock, registry); + + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.ACTIVATED) + .build())); } @Test @@ -200,6 +212,11 @@ void unpublish() { var did = doc.getId(); when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build()); when(publisherMock.unpublish(did)).thenReturn(Result.success()); + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.DEACTIVATED) + .build())); assertThat(service.unpublish(did)).isSucceeded(); @@ -226,6 +243,11 @@ void unpublish_noPublisherFound() { var did = doc.getId(); when(publisherRegistry.getPublisher(any())).thenReturn(null); when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build()); + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.DEACTIVATED) + .build())); assertThat(service.unpublish(did)).isFailed().detail() .isEqualTo(service.noPublisherFoundMessage(did)); @@ -241,7 +263,12 @@ void unpublish_publisherReportsError() { var did = doc.getId(); when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build()); when(publisherMock.unpublish(did)).thenReturn(Result.failure("test-failure")); - + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.DEACTIVATED) + .build())); + assertThat(service.unpublish(did)).isFailed() .detail() .isEqualTo("test-failure"); @@ -408,6 +435,12 @@ void onParticipantContextUpdated_whenDeactivates_shouldUnpublish() { when(didResourceStoreMock.query(any())).thenReturn(List.of(didResource)); when(publisherMock.unpublish(anyString())).thenReturn(Result.success()); + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.DEACTIVATED) + .build())); + service.on(EventEnvelope.Builder.newInstance() .payload(ParticipantContextUpdated.Builder.newInstance() .newState(ParticipantContextState.DEACTIVATED) @@ -454,6 +487,12 @@ void onParticipantContextUpdated_whenDeactivated_published_shouldBeNoop() { when(didResourceStoreMock.query(any())).thenReturn(List.of(didResource)); when(publisherMock.unpublish(anyString())).thenReturn(Result.success()); + when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(TEST_PARTICIPANT_ID) + .apiTokenAlias("token") + .state(ParticipantContextState.DEACTIVATED) + .build())); + service.on(EventEnvelope.Builder.newInstance() .payload(ParticipantContextUpdated.Builder.newInstance() .newState(ParticipantContextState.DEACTIVATED) @@ -572,10 +611,10 @@ public String name() { private DidDocument.Builder createDidDocument() { return DidDocument.Builder.newInstance() - .id("did:web:testdid") + .id(TEST_DID) .service(List.of(new Service("test-service", "test-service", "https://test.service.com/"))) .verificationMethod(List.of(VerificationMethod.Builder.newInstance() - .id("did:web:testdid#key-1") + .id(TEST_DID + "#key-1") .publicKeyMultibase("saflasjdflaskjdflasdkfj") .build())); } diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextCoordinatorExtension.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextCoordinatorExtension.java new file mode 100644 index 000000000..683695514 --- /dev/null +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextCoordinatorExtension.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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.identityhub.participantcontext; + +import org.eclipse.edc.identithub.spi.did.DidDocumentService; +import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; +import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextCreated; +import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleting; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.time.Clock; + +import static org.eclipse.edc.identityhub.participantcontext.ParticipantContextExtension.NAME; + +@Extension(NAME) +public class ParticipantContextCoordinatorExtension implements ServiceExtension { + public static final String NAME = "ParticipantContext Coordinator Extension"; + + @Inject + private DidDocumentService didDocumentService; + @Inject + private KeyPairService keyPairService; + @Inject + private Clock clock; + @Inject + private EventRouter eventRouter; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var coordinator = new ParticipantContextEventCoordinator(context.getMonitor().withPrefix("ParticipantContextEventCoordinator"), + didDocumentService, keyPairService); + + eventRouter.registerSync(ParticipantContextCreated.class, coordinator); + eventRouter.registerSync(ParticipantContextDeleting.class, coordinator); + } +} diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java index 689d031fb..9b7765d03 100644 --- a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java @@ -14,22 +14,17 @@ package org.eclipse.edc.identityhub.participantcontext; -import org.eclipse.edc.identithub.spi.did.DidDocumentService; import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; -import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextCreated; -import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleting; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextObservable; import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; -import org.eclipse.edc.keys.spi.KeyParserRegistry; 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.security.Vault; import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.transaction.spi.TransactionContext; import java.time.Clock; @@ -47,10 +42,6 @@ public class ParticipantContextExtension implements ServiceExtension { @Inject private TransactionContext transactionContext; @Inject - private KeyParserRegistry keyParserRegistry; - @Inject - private DidDocumentService didDocumentService; - @Inject private KeyPairService keyPairService; @Inject private Clock clock; @@ -66,15 +57,6 @@ public String name() { return NAME; } - @Override - public void initialize(ServiceExtensionContext context) { - var coordinator = new ParticipantContextEventCoordinator(context.getMonitor().withPrefix("ParticipantContextEventCoordinator"), - didDocumentService, keyPairService); - - eventRouter.registerSync(ParticipantContextCreated.class, coordinator); - eventRouter.registerSync(ParticipantContextDeleting.class, coordinator); - } - @Provider public ParticipantContextService createParticipantService() { return new ParticipantContextServiceImpl(participantContextStore, didResourceStore, vault, transactionContext, participantContextObservable()); diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java index 3fd8d6d98..89e553834 100644 --- a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java @@ -96,16 +96,20 @@ public ServiceResult deleteParticipantContext(String participantId) { if (participantContext == null) { return ServiceResult.notFound("A ParticipantContext with ID '%s' does not exist."); } - - observable.invokeForEach(l -> l.deleting(participantContext)); - var res = participantContextStore.deleteById(participantId); - vault.deleteSecret(participantContext.getApiTokenAlias()); - if (res.failed()) { - return fromFailure(res); - } - - observable.invokeForEach(l -> l.deleted(participantContext)); - return ServiceResult.success(); + // deactivating the PC must be the first step, because unpublishing DIDs requires the PC to be in the DEACTIVATED state. + // Unpublishing DIDs happens in callback of the "-Deleting" Event + return updateParticipant(participantId, ParticipantContext::deactivate) + .compose(v -> { + observable.invokeForEach(l -> l.deleting(participantContext)); + var res = participantContextStore.deleteById(participantId); + vault.deleteSecret(participantContext.getApiTokenAlias()); + if (res.failed()) { + return fromFailure(res); + } + + observable.invokeForEach(l -> l.deleted(participantContext)); + return ServiceResult.success(); + }); }); } diff --git a/core/identity-hub-participants/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-participants/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 07d76decc..80f523d19 100644 --- a/core/identity-hub-participants/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/identity-hub-participants/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -11,4 +11,5 @@ # Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation # # -org.eclipse.edc.identityhub.participantcontext.ParticipantContextExtension \ No newline at end of file +org.eclipse.edc.identityhub.participantcontext.ParticipantContextExtension +org.eclipse.edc.identityhub.participantcontext.ParticipantContextCoordinatorExtension \ No newline at end of file diff --git a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java index aef2f2d97..8ed2c38ff 100644 --- a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java +++ b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java @@ -221,10 +221,11 @@ void getParticipantContext_whenStorageFails() { void deleteParticipantContext() { when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); when(participantContextStore.deleteById(anyString())).thenReturn(StoreResult.success()); + when(participantContextStore.update(any())).thenReturn(StoreResult.success()); assertThat(participantContextService.deleteParticipantContext("test-id")).isSucceeded(); verify(participantContextStore).deleteById(anyString()); - verify(observableMock, times(2)).invokeForEach(any()); + verify(observableMock, times(3)).invokeForEach(any()); verify(vault).deleteSecret(anyString()); verifyNoMoreInteractions(vault, observableMock); } @@ -234,6 +235,8 @@ void deleteParticipantContext() { void deleteParticipantContext_whenNotExists() { when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); when(participantContextStore.deleteById(any())).thenReturn(StoreResult.notFound("foo bar")); + when(participantContextStore.update(any())).thenReturn(StoreResult.success()); + assertThat(participantContextService.deleteParticipantContext("test-id")) .isFailed() .satisfies(f -> { @@ -241,7 +244,7 @@ void deleteParticipantContext_whenNotExists() { Assertions.assertThat(f.getFailureDetail()).isEqualTo("foo bar"); }); - verify(observableMock).invokeForEach(any()); //deleting + verify(observableMock, times(2)).invokeForEach(any()); //deleting verify(participantContextStore).deleteById(anyString()); verify(vault).deleteSecret(anyString()); verifyNoMoreInteractions(vault, observableMock); diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java index fc43a9ecf..1b2670618 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java @@ -18,8 +18,10 @@ import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.identithub.spi.did.events.DidDocumentPublished; import org.eclipse.edc.identithub.spi.did.events.DidDocumentUnpublished; +import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; import org.eclipse.edc.junit.annotations.EndToEndTest; @@ -34,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import java.util.Arrays; +import java.util.List; import static io.restassured.http.ContentType.JSON; import static java.util.stream.IntStream.range; @@ -49,10 +52,16 @@ public class DidManagementApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService store) { - // purge all users - store.query(QuerySpec.max()).getContent() - .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId())); + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + // purge all users, dids, keypairs + + pcService.query(QuerySpec.max()).getContent() + .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); + + didResourceStore.query(QuerySpec.max()).forEach(dr -> didResourceStore.deleteById(dr.getDid()).getContent()); + + keyPairResourceStore.query(QuerySpec.max()).getContent() + .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); } @Test @@ -131,6 +140,38 @@ void publishDid(IdentityHubEndToEndTestContext context, EventRouter router) { } + + @Test + void publishDid_participantNotActivated_expect400(IdentityHubEndToEndTestContext context, EventRouter router) { + var superUserKey = context.createSuperUser(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentPublished.class, subscriber); + + var user = "test-user"; + var token = context.createParticipant(user, List.of(), false); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + reset(subscriber); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1alpha/participants/%s/dids/publish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(400) + .body(Matchers.containsString("Cannot publish DID 'did:web:test-user' for participant 'test-user' because the ParticipantContext is not state 'ACTIVATED' state, but was 'CREATED'.")); + + // verify that the publish event was fired twice + verifyNoInteractions(subscriber); + }); + } + @Test void unpublishDid_notOwner_expect403(IdentityHubEndToEndTestContext context, EventRouter router) { var subscriber = mock(EventSubscriber.class); @@ -170,7 +211,7 @@ void unpublishDid_notOwner_expect403(IdentityHubEndToEndTestContext context, Eve } @Test - void unpublishDid_withSuperUserToken(IdentityHubEndToEndTestContext context, EventRouter router) { + void unpublishDid_withSuperUserToken(IdentityHubEndToEndTestContext context, EventRouter router, ParticipantContextService participantContextService) { var superUserKey = context.createSuperUser(); var subscriber = mock(EventSubscriber.class); router.registerSync(DidDocumentUnpublished.class, subscriber); @@ -178,6 +219,8 @@ void unpublishDid_withSuperUserToken(IdentityHubEndToEndTestContext context, Eve var user = "test-user"; context.createParticipant(user); + participantContextService.updateParticipant(user, ParticipantContext::deactivate); + reset(subscriber); context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) @@ -194,22 +237,20 @@ void unpublishDid_withSuperUserToken(IdentityHubEndToEndTestContext context, Eve .body(Matchers.notNullValue()); // verify that the publish event was fired twice - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentUnpublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); + verifyNoInteractions(subscriber); } @Test - void unpublishDid_withUserToken(IdentityHubEndToEndTestContext context, EventRouter router) { + void unpublishDid_withUserToken(IdentityHubEndToEndTestContext context, EventRouter router, ParticipantContextService participantContextService) { var subscriber = mock(EventSubscriber.class); router.registerSync(DidDocumentUnpublished.class, subscriber); var user = "test-user"; var token = context.createParticipant(user); + participantContextService.updateParticipant(user, ParticipantContext::deactivate); + + reset(subscriber); context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) @@ -226,12 +267,34 @@ void unpublishDid_withUserToken(IdentityHubEndToEndTestContext context, EventRou .body(Matchers.notNullValue()); // verify that the unpublish event was fired - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentUnpublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); + verifyNoInteractions(subscriber); + } + + @Test + void unpublishDid_participantActive_expect400(IdentityHubEndToEndTestContext context, EventRouter router) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(DidDocumentUnpublished.class, subscriber); + + var user = "test-user"; + var token = context.createParticipant(user, List.of(), true); + + reset(subscriber); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", token)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1alpha/participants/%s/dids/unpublish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(400) + .body(Matchers.containsString("Cannot un-publish DID 'did:web:test-user' for participant 'test-user' because the ParticipantContext is not state 'DEACTIVATED' state, but was 'ACTIVATED'.")); + + // verify that the unpublish event was fired + verifyNoInteractions(subscriber); } @Test diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java index c23d3b62f..8eff17b15 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java @@ -15,12 +15,14 @@ package org.eclipse.edc.identityhub.tests; import io.restassured.http.Header; +import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; import org.eclipse.edc.junit.annotations.EndToEndTest; @@ -51,10 +53,16 @@ public class KeyPairResourceApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService store) { - // purge all users - store.query(QuerySpec.max()).getContent() - .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId()).getContent()); + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + // purge all users, dids, keypairs + + pcService.query(QuerySpec.max()).getContent() + .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); + + didResourceStore.query(QuerySpec.max()).forEach(dr -> didResourceStore.deleteById(dr.getDid()).getContent()); + + keyPairResourceStore.query(QuerySpec.max()).getContent() + .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); } @Test diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java index 861ccda82..a73686219 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java @@ -31,10 +31,12 @@ import org.eclipse.edc.iam.verifiablecredentials.spi.model.RevocationServiceRegistry; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubCustomizableEndToEndExtension; @@ -129,13 +131,16 @@ void setup(IdentityHubEndToEndTestContext context) { } @AfterEach - void teardown(ParticipantContextService contextService, CredentialStore store) { + void teardown(ParticipantContextService contextService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, CredentialStore store) { // purge all participant contexts - contextService.query(QuerySpec.none()) - .onSuccess(participantContexts -> participantContexts.forEach(participant -> { - contextService.deleteParticipantContext(participant.getParticipantId()); - })) - .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + + contextService.query(QuerySpec.max()).getContent() + .forEach(pc -> contextService.deleteParticipantContext(pc.getParticipantId()).getContent()); + + didResourceStore.query(QuerySpec.max()).forEach(dr -> didResourceStore.deleteById(dr.getDid()).getContent()); + + keyPairResourceStore.query(QuerySpec.max()).getContent() + .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); // purge all VCs store.query(QuerySpec.none()) diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java index 36b16aebd..d95051438 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java @@ -18,7 +18,9 @@ import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; @@ -44,10 +46,16 @@ public class VerifiableCredentialApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService store) { - // purge all users - store.query(QuerySpec.max()).getContent() - .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId()).getContent()); + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + // purge all users, dids, keypairs + + pcService.query(QuerySpec.max()).getContent() + .forEach(pc -> pcService.deleteParticipantContext(pc.getParticipantId()).getContent()); + + didResourceStore.query(QuerySpec.max()).forEach(dr -> didResourceStore.deleteById(dr.getDid()).getContent()); + + keyPairResourceStore.query(QuerySpec.max()).getContent() + .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); } @Test