From 1b426a850d463c9e83e4c1961faaef37fff3d287 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:48:51 +0200 Subject: [PATCH] feat: implement StsAccountProvisioner (#458) * feat: implement StsClientProvisioner * handle deleted event * DEPENDENCIES * added StsClientSecretGenerator * add embedded STS and STS API to IH runtime * AccountProvisioner is called explicitly by ParticipantContextService * implement key-revoked and key-rotated callbacks * add new keypair descriptor to revoke/rotate event * make provisioner transactional * pr remarks * updated wording --- DEPENDENCIES | 6 +- .../did/DidDocumentServiceImpl.java | 14 +- .../identityhub/did/DidServicesExtension.java | 11 +- .../did/DidDocumentServiceImplTest.java | 22 +-- .../keypairs/KeyPairEventPublisher.java | 16 +- .../keypairs/KeyPairServiceImpl.java | 11 +- .../ParticipantContextExtension.java | 12 +- .../ParticipantContextServiceImpl.java | 46 +++-- .../ParticipantContextServiceImplTest.java | 50 ++--- e2e-tests/api-tests/build.gradle.kts | 1 + .../tests/DidManagementApiEndToEndTest.java | 6 +- .../tests/KeyPairResourceApiEndToEndTest.java | 171 ++++++++++++++--- .../ParticipantContextApiEndToEndTest.java | 10 +- .../tests/PresentationApiEndToEndTest.java | 12 +- .../VerifiableCredentialApiEndToEndTest.java | 7 +- .../IdentityHubEndToEndTestContext.java | 2 +- .../IdentityHubRuntimeConfiguration.java | 2 + .../v1/unstable/ParticipantContextApi.java | 3 +- .../ParticipantContextApiController.java | 3 +- .../sts-account-provisioner/build.gradle.kts | 29 +++ .../provisioner/StsAccountProvisioner.java | 160 ++++++++++++++++ .../StsAccountProvisionerExtension.java | 114 ++++++++++++ .../provisioner/StsClientSecretGenerator.java | 30 +++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 ++ .../StsAccountProvisionerTest.java | 175 ++++++++++++++++++ gradle/libs.versions.toml | 5 + launcher/build.gradle.kts | 4 + settings.gradle.kts | 1 + .../spi/store/ParticipantContextStore.java | 11 ++ .../spi/keypair/events/KeyPairEvent.java | 12 +- .../keypair/events/KeyPairEventListener.java | 12 +- .../spi/keypair/events/KeyPairRevoked.java | 13 ++ .../spi/keypair/events/KeyPairRotated.java | 13 ++ .../spi/keypair/model/KeyPairResource.java | 6 + .../spi/keypair/events/KeyPairAddedTest.java | 5 +- .../keypair/events/KeyPairRevokedTest.java | 5 +- .../keypair/events/KeyPairRotatedTest.java | 5 +- .../spi/participantcontext/AccountInfo.java | 18 ++ .../AccountProvisioner.java | 22 +++ .../ParticipantContextService.java | 3 +- 40 files changed, 927 insertions(+), 136 deletions(-) create mode 100644 extensions/common/sts-account-provisioner/build.gradle.kts create mode 100644 extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisioner.java create mode 100644 extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerExtension.java create mode 100644 extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsClientSecretGenerator.java create mode 100644 extensions/common/sts-account-provisioner/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 extensions/common/sts-account-provisioner/src/test/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerTest.java create mode 100644 spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountInfo.java create mode 100644 spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountProvisioner.java diff --git a/DEPENDENCIES b/DEPENDENCIES index e488ebcd6..ef3279ab6 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -72,7 +72,7 @@ maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefine maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, Apache-2.0, approved, #15156 -maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.41, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.41.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.1, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #16060 maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 @@ -255,6 +255,10 @@ maven/mavencentral/org.eclipse.edc/identity-did-core/0.10.0-SNAPSHOT, Apache-2.0 maven/mavencentral/org.eclipse.edc/identity-did-spi/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-did-web/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-trust-spi/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/identity-trust-sts-api/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/identity-trust-sts-core/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/identity-trust-sts-embedded/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/identity-trust-sts-spi/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/identity-trust-transform/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-core/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/jersey-providers-lib/0.10.0-SNAPSHOT, Apache-2.0, approved, technology.edc 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 e57799361..2b1a2ceb9 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,11 +24,11 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairActivated; 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.identityhub.spi.store.ParticipantContextStore; import org.eclipse.edc.keys.spi.KeyParserRegistry; import org.eclipse.edc.security.token.jwt.CryptoConverter; import org.eclipse.edc.spi.event.Event; @@ -57,16 +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 ParticipantContextStore participantContextStore; private final Monitor monitor; private final KeyParserRegistry keyParserRegistry; public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry, - ParticipantContextService participantContextService, Monitor monitor, KeyParserRegistry keyParserRegistry) { + ParticipantContextStore participantContextStore, Monitor monitor, KeyParserRegistry keyParserRegistry) { this.transactionContext = transactionContext; this.didResourceStore = didResourceStore; this.registry = registry; - this.participantContextService = participantContextService; + this.participantContextStore = participantContextStore; this.monitor = monitor; this.keyParserRegistry = keyParserRegistry; } @@ -112,7 +112,7 @@ public ServiceResult publish(String did) { return ServiceResult.notFound(notFoundMessage(did)); } var participantId = existingResource.getParticipantId(); - return participantContextService.getParticipantContext(participantId) + return ServiceResult.from(participantContextStore.findById(participantId)) .map(ParticipantContext::getStateAsEnum) .compose(state -> { var canPublish = state.equals(ParticipantContextState.ACTIVATED); @@ -142,7 +142,7 @@ public ServiceResult unpublish(String did) { } var participantId = existingResource.getParticipantId(); - return participantContextService.getParticipantContext(participantId) + return ServiceResult.from(participantContextStore.findById(participantId)) .map(ParticipantContext::getStateAsEnum) .compose(state -> { var canUnpublish = state.equals(ParticipantContextState.DEACTIVATED); @@ -267,7 +267,7 @@ private void keyPairActivated(KeyPairActivated event) { var publicKey = keyParserRegistry.parse(serialized); if (publicKey.failed()) { - monitor.warning("Error adding KeyPair '%s' to DID Document of participant '%s': %s".formatted(event.getKeyPairResourceId(), event.getParticipantId(), publicKey.getFailureDetail())); + monitor.warning("Error adding KeyPair '%s' to DID Document of participant '%s': %s".formatted(event.getKeyPairResource().getId(), event.getParticipantId(), publicKey.getFailureDetail())); return; } 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 4ddfe61cd..fda3ac63e 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,8 +19,8 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairActivated; 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.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; @@ -39,16 +39,13 @@ public class DidServicesExtension implements ServiceExtension { private TransactionContext transactionContext; @Inject private DidResourceStore didResourceStore; - @Inject private EventRouter eventRouter; - - private DidDocumentPublisherRegistry didPublisherRegistry; - @Inject private KeyParserRegistry keyParserRegistry; @Inject - private ParticipantContextService participantContextService; + private ParticipantContextStore participantContextStore; + private DidDocumentPublisherRegistry didPublisherRegistry; @Override public String name() { @@ -66,7 +63,7 @@ public DidDocumentPublisherRegistry getDidPublisherRegistry() { @Provider public DidDocumentService createDidDocumentService(ServiceExtensionContext context) { var service = new DidDocumentServiceImpl(transactionContext, didResourceStore, - getDidPublisherRegistry(), participantContextService, context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry); + getDidPublisherRegistry(), participantContextStore, context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry); eventRouter.registerSync(ParticipantContextUpdated.class, service); eventRouter.registerSync(KeyPairRevoked.class, service); eventRouter.registerSync(KeyPairActivated.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 58f064cf2..faa46bb59 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,10 +28,11 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairActivated; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; -import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; 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.store.ParticipantContextStore; import org.eclipse.edc.keys.KeyParserRegistryImpl; import org.eclipse.edc.keys.keyparsers.JwkParser; import org.eclipse.edc.keys.keyparsers.PemParser; @@ -40,7 +41,6 @@ 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; @@ -68,7 +68,7 @@ class DidDocumentServiceImplTest { private final DidResourceStore didResourceStoreMock = mock(); private final DidDocumentPublisherRegistry publisherRegistry = mock(); private final DidDocumentPublisher publisherMock = mock(); - private final ParticipantContextService participantContextServiceMock = mock(); + private final ParticipantContextStore participantContextServiceMock = mock(); private DidDocumentServiceImpl service; private Monitor monitorMock; @@ -83,7 +83,7 @@ void setUp() { monitorMock = mock(); service = new DidDocumentServiceImpl(trx, didResourceStoreMock, publisherRegistry, participantContextServiceMock, monitorMock, registry); - when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.ACTIVATED) @@ -212,7 +212,7 @@ 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() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.DEACTIVATED) @@ -243,7 +243,7 @@ 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() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.DEACTIVATED) @@ -263,7 +263,7 @@ 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() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.DEACTIVATED) @@ -435,7 +435,7 @@ 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() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.DEACTIVATED) @@ -487,7 +487,7 @@ 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() + when(participantContextServiceMock.findById(any())).thenReturn(StoreResult.success(ParticipantContext.Builder.newInstance() .participantId(TEST_PARTICIPANT_ID) .apiTokenAlias("token") .state(ParticipantContextState.DEACTIVATED) @@ -547,7 +547,7 @@ void onKeyPairActivated() throws JOSEException { .id(UUID.randomUUID().toString()) .payload(KeyPairActivated.Builder.newInstance() .keyId(keyId) - .keyPairResourceId("test-resource-id") + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) .participantId("test-participant") .publicKey(key.toPublicJWK().toJSONString(), JSON_WEB_KEY_2020) .build()) @@ -582,7 +582,7 @@ void onKeyPairRevoked() throws JOSEException { .id(UUID.randomUUID().toString()) .payload(KeyPairRevoked.Builder.newInstance() .keyId(keyId) - .keyPairResourceId("test-resource-id") + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) .participantId("test-participant") .build()) .build(); diff --git a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairEventPublisher.java b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairEventPublisher.java index 1a3863d74..6f15c0e8f 100644 --- a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairEventPublisher.java +++ b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairEventPublisher.java @@ -21,8 +21,10 @@ import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; import org.eclipse.edc.spi.event.EventEnvelope; import org.eclipse.edc.spi.event.EventRouter; +import org.jetbrains.annotations.Nullable; import java.time.Clock; @@ -39,7 +41,7 @@ public KeyPairEventPublisher(Clock clock, EventRouter eventRouter) { public void added(KeyPairResource keyPair, String type) { var event = KeyPairAdded.Builder.newInstance() .participantId(keyPair.getParticipantId()) - .keyPairResourceId(keyPair.getId()) + .keyPairResource(keyPair) .keyId(keyPair.getKeyId()) .publicKey(keyPair.getSerializedPublicKey(), type) .build(); @@ -47,21 +49,23 @@ public void added(KeyPairResource keyPair, String type) { } @Override - public void rotated(KeyPairResource keyPair) { + public void rotated(KeyPairResource keyPair, @Nullable KeyDescriptor newKeyDesc) { var event = KeyPairRotated.Builder.newInstance() .participantId(keyPair.getParticipantId()) - .keyPairResourceId(keyPair.getId()) + .keyPairResource(keyPair) .keyId(keyPair.getKeyId()) + .newKeyDescriptor(newKeyDesc) .build(); publish(event); } @Override - public void revoked(KeyPairResource keyPair) { + public void revoked(KeyPairResource keyPair, @Nullable KeyDescriptor newKeyDesc) { var event = KeyPairRevoked.Builder.newInstance() .participantId(keyPair.getParticipantId()) - .keyPairResourceId(keyPair.getId()) + .keyPairResource(keyPair) .keyId(keyPair.getKeyId()) + .newKeyDescriptor(newKeyDesc) .build(); publish(event); } @@ -70,7 +74,7 @@ public void revoked(KeyPairResource keyPair) { public void activated(KeyPairResource activatedKeyPair, String type) { var event = KeyPairActivated.Builder.newInstance() .participantId(activatedKeyPair.getParticipantId()) - .keyPairResourceId(activatedKeyPair.getId()) + .keyPairResource(activatedKeyPair) .publicKey(activatedKeyPair.getSerializedPublicKey(), type) .keyId(activatedKeyPair.getKeyId()) .build(); diff --git a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java index e113345d8..5a6cd8030 100644 --- a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java +++ b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java @@ -89,7 +89,6 @@ public ServiceResult addKeyPair(String participantId, KeyDescriptor keyDes // check if the new key is not active, and no other active key exists if (!keyDescriptor.isActive()) { - //todo: replace this with invocation to activateKeyPair() var hasActiveKeys = keyPairResourceStore.query(ParticipantResource.queryByParticipantId(participantId).build()) .orElse(failure -> Collections.emptySet()) .stream().filter(kpr -> kpr.getState() == KeyPairState.ACTIVATED.code()) @@ -140,7 +139,7 @@ public ServiceResult rotateKeyPair(String oldId, @Nullable KeyDescriptor n vault.deleteSecret(oldAlias); oldKey.rotate(duration); var updateResult = ServiceResult.from(keyPairResourceStore.update(oldKey)) - .onSuccess(v -> observable.invokeForEach(l -> l.rotated(oldKey))); + .onSuccess(v -> observable.invokeForEach(l -> l.rotated(oldKey, newKeyDesc))); if (newKeyDesc != null) { return updateResult.compose(v -> addKeyPair(participantId, newKeyDesc, wasDefault)); @@ -151,7 +150,7 @@ public ServiceResult rotateKeyPair(String oldId, @Nullable KeyDescriptor n } @Override - public ServiceResult revokeKey(String id, @Nullable KeyDescriptor newKeySpec) { + public ServiceResult revokeKey(String id, @Nullable KeyDescriptor newKeyDesc) { return transactionContext.execute(() -> { var oldKey = findById(id); if (oldKey == null) { @@ -166,10 +165,10 @@ public ServiceResult revokeKey(String id, @Nullable KeyDescriptor newKeySp vault.deleteSecret(oldAlias); oldKey.revoke(); var updateResult = ServiceResult.from(keyPairResourceStore.update(oldKey)) - .onSuccess(v -> observable.invokeForEach(l -> l.revoked(oldKey))); + .onSuccess(v -> observable.invokeForEach(l -> l.revoked(oldKey, newKeyDesc))); - if (newKeySpec != null) { - return updateResult.compose(v -> addKeyPair(participantId, newKeySpec, wasDefault)); + if (newKeyDesc != null) { + return updateResult.compose(v -> addKeyPair(participantId, newKeyDesc, wasDefault)); } monitor.warning("Revoking keys without a successor key may leave the participant without an active keypair."); return updateResult; 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 9b7765d03..e6298e3e3 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 @@ -16,6 +16,7 @@ import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountProvisioner; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextObservable; import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; @@ -29,6 +30,7 @@ import java.time.Clock; +import static java.util.Optional.ofNullable; import static org.eclipse.edc.identityhub.participantcontext.ParticipantContextExtension.NAME; @Extension(NAME) @@ -50,6 +52,9 @@ public class ParticipantContextExtension implements ServiceExtension { @Inject private DidResourceStore didResourceStore; + @Inject(required = false) + private AccountProvisioner accountProvisioner; + private ParticipantContextObservable participantContextObservable; @Override @@ -59,7 +64,7 @@ public String name() { @Provider public ParticipantContextService createParticipantService() { - return new ParticipantContextServiceImpl(participantContextStore, didResourceStore, vault, transactionContext, participantContextObservable()); + return new ParticipantContextServiceImpl(participantContextStore, didResourceStore, vault, transactionContext, participantContextObservable(), accountProvisioner()); } @Provider @@ -70,4 +75,9 @@ public ParticipantContextObservable participantContextObservable() { } return participantContextObservable; } + + private AccountProvisioner accountProvisioner() { + return ofNullable(accountProvisioner) + .orElseGet(() -> manifest -> null); // default is a NOOP + } } 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 587c4d693..55276f4b1 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 @@ -15,12 +15,12 @@ package org.eclipse.edc.identityhub.participantcontext; import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountProvisioner; import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextObservable; 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.ParticipantManifest; -import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; @@ -28,7 +28,8 @@ import org.eclipse.edc.transaction.spi.TransactionContext; import java.util.Collection; -import java.util.concurrent.atomic.AtomicReference; +import java.util.HashMap; +import java.util.Map; import java.util.function.Consumer; import static org.eclipse.edc.spi.result.ServiceResult.conflict; @@ -51,42 +52,48 @@ public class ParticipantContextServiceImpl implements ParticipantContextService private final TransactionContext transactionContext; private final ApiTokenGenerator tokenGenerator; private final ParticipantContextObservable observable; - - public ParticipantContextServiceImpl(ParticipantContextStore participantContextStore, DidResourceStore didResourceStore, Vault vault, TransactionContext transactionContext, ParticipantContextObservable observable) { + private final AccountProvisioner accountProvisioner; + + public ParticipantContextServiceImpl(ParticipantContextStore participantContextStore, + DidResourceStore didResourceStore, + Vault vault, + TransactionContext transactionContext, + ParticipantContextObservable observable, + AccountProvisioner accountProvisioner) { this.participantContextStore = participantContextStore; this.didResourceStore = didResourceStore; this.vault = vault; this.transactionContext = transactionContext; this.observable = observable; + this.accountProvisioner = accountProvisioner; this.tokenGenerator = new ApiTokenGenerator(); } @Override - public ServiceResult createParticipantContext(ParticipantManifest manifest) { + public ServiceResult> createParticipantContext(ParticipantManifest manifest) { return transactionContext.execute(() -> { if (didResourceStore.findById(manifest.getDid()) != null) { return ServiceResult.conflict("Another participant with the same DID '%s' already exists.".formatted(manifest.getDid())); } - var apiKey = new AtomicReference(); + var response = new HashMap(); var context = convert(manifest); var res = createParticipantContext(context) - .compose(u -> createTokenAndStoreInVault(context)).onSuccess(apiKey::set) + .compose(u -> createTokenAndStoreInVault(context)).onSuccess(k -> response.put("apiKey", k)) + .compose(apiKey -> accountProvisioner.create(manifest)) + .onSuccess(accountInfo -> { + if (accountInfo != null) { + response.put("clientId", accountInfo.clientId()); + response.put("clientSecret", accountInfo.clientSecret()); + } + }) .onSuccess(apiToken -> observable.invokeForEach(l -> l.created(context, manifest))); - return res.map(u -> apiKey.get()); + return res.map(u -> response); }); } @Override public ServiceResult getParticipantContext(String participantId) { - return transactionContext.execute(() -> { - var res = participantContextStore.query(ParticipantResource.queryByParticipantId(participantId).build()); - if (res.succeeded()) { - return res.getContent().stream().findFirst() - .map(ServiceResult::success) - .orElse(notFound("ParticipantContext with ID '%s' does not exist.".formatted(participantId))); - } - return fromFailure(res); - }); + return transactionContext.execute(() -> ServiceResult.from(participantContextStore.findById(participantId))); } @Override @@ -161,9 +168,8 @@ private ServiceResult createParticipantContext(ParticipantContext context) } private ParticipantContext findByIdInternal(String participantId) { - var resultStream = participantContextStore.query(ParticipantResource.queryByParticipantId(participantId).build()); - if (resultStream.failed()) return null; - return resultStream.getContent().stream().findFirst().orElse(null); + var resultStream = participantContextStore.findById(participantId); + return resultStream.orElse(f -> null); } 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 8ed2c38ff..5521a4fcf 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 @@ -20,6 +20,7 @@ import org.assertj.core.api.Assertions; import org.eclipse.edc.identithub.spi.did.model.DidResource; import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountProvisioner; import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextObservable; import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; @@ -31,6 +32,7 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.result.ServiceFailure; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.security.Vault; import org.eclipse.edc.transaction.spi.NoopTransactionContext; @@ -60,13 +62,15 @@ class ParticipantContextServiceImplTest { private final ParticipantContextStore participantContextStore = mock(); private final ParticipantContextObservable observableMock = mock(); private final DidResourceStore didResourceStore = mock(); + private final AccountProvisioner provisionerMock = mock(); private ParticipantContextServiceImpl participantContextService; @BeforeEach void setUp() { var keyParserRegistry = new KeyParserRegistryImpl(); keyParserRegistry.register(new PemParser(mock())); - participantContextService = new ParticipantContextServiceImpl(participantContextStore, didResourceStore, vault, new NoopTransactionContext(), observableMock); + participantContextService = new ParticipantContextServiceImpl(participantContextStore, didResourceStore, vault, new NoopTransactionContext(), observableMock, provisionerMock); + when(provisionerMock.create(any())).thenReturn(ServiceResult.success()); } @ParameterizedTest(name = "isActive: {0}") @@ -178,34 +182,34 @@ void createParticipantContext_whenDidExists() { @Test void getParticipantContext() { var ctx = createContext(); - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(ctx))); + when(participantContextStore.findById(any())).thenReturn(StoreResult.success(ctx)); assertThat(participantContextService.getParticipantContext("test-id")) .isSucceeded() .usingRecursiveComparison() .isEqualTo(ctx); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verifyNoMoreInteractions(vault); } @Test void getParticipantContext_whenNotExists() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of())); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.notFound("foo")); assertThat(participantContextService.getParticipantContext("test-id")) .isFailed() .satisfies(f -> { Assertions.assertThat(f.getReason()).isEqualTo(ServiceFailure.Reason.NOT_FOUND); - Assertions.assertThat(f.getFailureDetail()).isEqualTo("ParticipantContext with ID 'test-id' does not exist."); + Assertions.assertThat(f.getFailureDetail()).isEqualTo("foo"); }); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verifyNoMoreInteractions(vault); } @Test void getParticipantContext_whenStorageFails() { - when(participantContextStore.query(any())).thenReturn(StoreResult.notFound("foo bar")); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.notFound("foo bar")); assertThat(participantContextService.getParticipantContext("test-id")) .isFailed() .satisfies(f -> { @@ -213,13 +217,13 @@ void getParticipantContext_whenStorageFails() { Assertions.assertThat(f.getFailureDetail()).isEqualTo("foo bar"); }); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verifyNoMoreInteractions(vault); } @Test void deleteParticipantContext() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(createContext())); when(participantContextStore.deleteById(anyString())).thenReturn(StoreResult.success()); when(participantContextStore.update(any())).thenReturn(StoreResult.success()); assertThat(participantContextService.deleteParticipantContext("test-id")).isSucceeded(); @@ -233,7 +237,7 @@ void deleteParticipantContext() { @Test void deleteParticipantContext_whenNotExists() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(createContext())); when(participantContextStore.deleteById(any())).thenReturn(StoreResult.notFound("foo bar")); when(participantContextStore.update(any())).thenReturn(StoreResult.success()); @@ -252,44 +256,44 @@ void deleteParticipantContext_whenNotExists() { @Test void regenerateApiToken() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(createContext())); when(vault.storeSecret(eq("test-alias"), anyString())).thenReturn(Result.success()); assertThat(participantContextService.regenerateApiToken("test-id")).isSucceeded().isNotNull(); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verify(vault).storeSecret(eq("test-alias"), argThat(s -> s.length() >= 64)); } @Test void regenerateApiToken_vaultFails() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(createContext()))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(createContext())); when(vault.storeSecret(eq("test-alias"), anyString())).thenReturn(Result.failure("test failure")); assertThat(participantContextService.regenerateApiToken("test-id")).isFailed().detail().isEqualTo("Could not store new API token: test failure."); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verify(vault).storeSecret(eq("test-alias"), anyString()); } @Test void regenerateApiToken_whenNotFound() { - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of())); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.notFound("foo")); - assertThat(participantContextService.regenerateApiToken("test-id")).isFailed().detail().isEqualTo("ParticipantContext with ID 'test-id' does not exist."); + assertThat(participantContextService.regenerateApiToken("test-id")).isFailed().detail().isEqualTo("foo"); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verifyNoMoreInteractions(participantContextStore, vault); } @Test void update() { var context = createContext(); - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(context))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(context)); when(participantContextStore.update(any())).thenReturn(StoreResult.success()); assertThat(participantContextService.updateParticipant(context.getParticipantId(), ParticipantContext::deactivate)).isSucceeded(); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verify(participantContextStore).update(any()); verify(observableMock).invokeForEach(any()); } @@ -297,24 +301,24 @@ void update() { @Test void update_whenNotFound() { var context = createContext(); - when(participantContextStore.query(any())).thenReturn(StoreResult.notFound("foobar")); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.notFound("foobar")); assertThat(participantContextService.updateParticipant(context.getParticipantId(), ParticipantContext::deactivate)).isFailed() .detail().isEqualTo("ParticipantContext with ID 'test-id' not found."); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verifyNoMoreInteractions(participantContextStore, observableMock); } @Test void update_whenStoreUpdateFails() { var context = createContext(); - when(participantContextStore.query(any())).thenReturn(StoreResult.success(List.of(context))); + when(participantContextStore.findById(anyString())).thenReturn(StoreResult.success(context)); when(participantContextStore.update(any())).thenReturn(StoreResult.alreadyExists("test-msg")); assertThat(participantContextService.updateParticipant(context.getParticipantId(), ParticipantContext::deactivate)).isFailed() .detail().isEqualTo("test-msg"); - verify(participantContextStore).query(any()); + verify(participantContextStore).findById(anyString()); verify(participantContextStore).update(any()); verifyNoMoreInteractions(participantContextStore, observableMock); } diff --git a/e2e-tests/api-tests/build.gradle.kts b/e2e-tests/api-tests/build.gradle.kts index 235af7af4..b9719711d 100644 --- a/e2e-tests/api-tests/build.gradle.kts +++ b/e2e-tests/api-tests/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { testImplementation(libs.edc.sql.pool) testImplementation(libs.nimbus.jwt) testImplementation(libs.jakarta.rsApi) + testImplementation(libs.edc.sts.spi) } edcBuild { 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 6dd84abdb..e671a0a5d 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 @@ -16,6 +16,7 @@ import io.restassured.http.Header; import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; 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; @@ -52,7 +53,7 @@ public class DidManagementApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, StsClientStore stsClientStore) { // purge all users, dids, keypairs pcService.query(QuerySpec.max()).getContent() @@ -62,6 +63,9 @@ void tearDown(ParticipantContextService pcService, DidResourceStore didResourceS keyPairResourceStore.query(QuerySpec.max()).getContent() .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); + + stsClientStore.findAll(QuerySpec.max()) + .forEach(sts -> stsClientStore.deleteById(sts.getId()).getContent()); } @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 231021de4..da0475479 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 @@ -16,11 +16,13 @@ import io.restassured.http.ContentType; import io.restassured.http.Header; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; import org.eclipse.edc.identithub.spi.did.events.DidDocumentPublished; import org.eclipse.edc.identithub.spi.did.model.DidState; import org.eclipse.edc.identithub.spi.did.store.DidResourceStore; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairActivated; import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; 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; @@ -39,6 +41,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.Arrays; import java.util.Base64; @@ -48,6 +52,7 @@ import static io.restassured.http.ContentType.JSON; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.argThat; @@ -63,7 +68,7 @@ public class KeyPairResourceApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, StsClientStore stsClientStore) { // purge all users, dids, keypairs pcService.query(QuerySpec.max()).getContent() @@ -73,6 +78,9 @@ void tearDown(ParticipantContextService pcService, DidResourceStore didResourceS keyPairResourceStore.query(QuerySpec.max()).getContent() .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); + + stsClientStore.findAll(QuerySpec.max()) + .forEach(sts -> stsClientStore.deleteById(sts.getId()).getContent()); } @Test @@ -200,7 +208,7 @@ void addKeyPair(IdentityHubEndToEndTestContext context, EventRouter router) { verify(subscriber).on(argThat(env -> { var evt = (KeyPairAdded) env.getPayload(); return evt.getParticipantId().equals(participantId) && - evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && + evt.getKeyPairResource().getId().equals(keyDesc.getResourceId()) && evt.getKeyId().equals(keyDesc.getKeyId()); })); }); @@ -232,7 +240,7 @@ void addKeyPair_notAuthorized(IdentityHubEndToEndTestContext context, EventRoute verify(subscriber, never()).on(argThat(env -> { if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); + return evt.getKeyPairResource().equals(keyDesc.getKeyId()); } return false; })); @@ -358,31 +366,35 @@ void rotate_withSuperUserToken(IdentityHubEndToEndTestContext context, EventRout // verify that the correct "added" event fired verify(subscriber).on(argThat(env -> { if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && + return evt.getKeyPairResource().getId().equals(keyDesc.getResourceId()) && evt.getKeyId().equals(keyDesc.getKeyId()); } return false; })); } - @Test - void rotate_withUserToken(IdentityHubEndToEndTestContext context, EventRouter router) { + @ParameterizedTest(name = "New KeyID {0}") + @ValueSource(strings = { "did:web:user1#new-key-id", "new-key-id" }) + void rotate_withUserToken(String keyId, IdentityHubEndToEndTestContext context, EventRouter router, StsClientStore clientStore) { var subscriber = mock(EventSubscriber.class); router.registerSync(KeyPairRotated.class, subscriber); router.registerSync(KeyPairAdded.class, subscriber); - var user1 = "user1"; - var userToken = context.createParticipant(user1); + var participantId = "user1"; + var userToken = context.createParticipant(participantId); - var keyPairId = context.createKeyPair(user1).getResourceId(); + var keyPairId = context.createKeyPair(participantId).getResourceId(); // attempt to publish user1's DID document, which should fail - var keyDesc = context.createKeyDescriptor(user1).build(); + var keyDesc = context.createKeyDescriptor(participantId) + .privateKeyAlias("new-key-alias") + .keyId(keyId) + .build(); context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) .header(new Header("x-api-key", userToken)) .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId)) + .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(participantId), keyPairId)) .then() .log().ifValidationFails() .statusCode(204) @@ -391,18 +403,66 @@ void rotate_withUserToken(IdentityHubEndToEndTestContext context, EventRouter ro // verify that the "rotated" event fired once verify(subscriber).on(argThat(env -> { if (env.getPayload() instanceof KeyPairRotated evt) { - return evt.getParticipantId().equals(user1); + return evt.getParticipantId().equals(participantId); } return false; })); // verify that the correct "added" event fired verify(subscriber).on(argThat(env -> { if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) && + return evt.getKeyPairResource().getId().equals(keyDesc.getResourceId()) && evt.getKeyId().equals(keyDesc.getKeyId()); } return false; })); + + // verify that the STS client got updated correctly + assertThat(clientStore.findById(participantId)).isSucceeded() + .satisfies(stsClient -> { + assertThat(stsClient.getPrivateKeyAlias()).isEqualTo("new-key-alias"); + assertThat(stsClient.getPublicKeyReference()).isEqualTo("did:web:" + participantId + "#new-key-id"); + }); + } + + @Test + void rotate_withoutNewKey(IdentityHubEndToEndTestContext context, EventRouter router, StsClientStore clientStore) { + + var participantId = "user1"; + var userToken = context.createParticipant(participantId); + + var keyPairId = context.createKeyPair(participantId).getResourceId(); + + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairRotated.class, subscriber); + router.registerSync(KeyPairAdded.class, subscriber); + + + // attempt to publish user1's DID document, which should fail + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", userToken)) + .post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(participantId), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + // verify that the "rotated" event fired once + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairRotated evt) { + return evt.getParticipantId().equals(participantId); + } + return false; + })); + // verify that the correct "added" event fired + verify(subscriber, never()).on(argThat(env -> env.getPayload() instanceof KeyPairAdded)); + + // verify that the STS client got updated correctly + assertThat(clientStore.findById(participantId)).isSucceeded() + .satisfies(stsClient -> { + assertThat(stsClient.getPrivateKeyAlias()).isEqualTo(""); + assertThat(stsClient.getPublicKeyReference()).isEqualTo(""); + }); } @Test @@ -433,7 +493,7 @@ void rotate_notAuthorized(IdentityHubEndToEndTestContext context, EventRouter ro // make sure that the event to add the _new_ keypair was never fired verify(subscriber, never()).on(argThat(env -> { if (env.getPayload() instanceof KeyPairRotated evt) { - return evt.getParticipantId().equals(user1) && evt.getKeyPairResourceId().equals(keyDesc.getKeyId()); + return evt.getParticipantId().equals(user1) && evt.getKeyPairResource().equals(keyDesc.getKeyId()); } return false; })); @@ -441,14 +501,14 @@ void rotate_notAuthorized(IdentityHubEndToEndTestContext context, EventRouter ro @Test void rotate_withNewKey_shouldUpdateDidDocument(IdentityHubEndToEndTestContext context, EventRouter router, Vault vault) { - var subscriber = mock(EventSubscriber.class); - router.registerSync(KeyPairRotated.class, subscriber); - router.registerSync(KeyPairAdded.class, subscriber); - var participantId = "user1"; var userToken = context.createParticipant(participantId); var keyPair = context.getKeyPairsForParticipant(participantId).stream().findFirst().orElseThrow(); + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairRotated.class, subscriber); + router.registerSync(KeyPairAdded.class, subscriber); + var originalAlias = participantId + "-alias"; var originalKeyId = participantId + "-key"; var newPrivateKeyAlias = "new-alias"; @@ -468,6 +528,8 @@ void rotate_withNewKey_shouldUpdateDidDocument(IdentityHubEndToEndTestContext co .statusCode(204) .body(notNullValue()); + verify(subscriber).on(argThat(evt -> evt.getPayload() instanceof KeyPairRotated)); + verify(subscriber).on(argThat(evt -> evt.getPayload() instanceof KeyPairAdded)); var didDoc = context.getDidForParticipant(participantId); assertThat(didDoc).isNotEmpty() .allSatisfy(doc -> assertThat(doc.getVerificationMethod()).hasSize(2) @@ -519,29 +581,80 @@ void rotate_withNewKey_whenDidNotPublished_shouldNotUpdate(IdentityHubEndToEndTe verify(subscriber, never()).on(argThat(evt -> evt.getPayload() instanceof DidDocumentPublished)); } - @Test - void revoke(IdentityHubEndToEndTestContext context) { + @ParameterizedTest(name = "New Key-ID: {0}") + @ValueSource(strings = { "new-keyId", "did:web:user1#new-keyId" }) + void revoke(String newKeyId, IdentityHubEndToEndTestContext context, StsClientStore clientStore) { var superUserKey = context.createSuperUser(); - var user1 = "user1"; - var token = context.createParticipant(user1); + var participantId = "user1"; + var token = context.createParticipant(participantId); - var keyId = context.createKeyPair(user1).getResourceId(); + var keyId = context.createKeyPair(participantId).getResourceId(); assertThat(Arrays.asList(token, superUserKey)) .allSatisfy(t -> { - var keyDesc = context.createKeyDescriptor(user1).build(); + var keyDesc = context.createKeyDescriptor(participantId) + .privateKeyAlias("new-alias") + .keyId(newKeyId) + .build(); + context.getIdentityApiEndpoint().baseRequest() .contentType(JSON) .header(new Header("x-api-key", t)) .body(keyDesc) - .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(user1), keyId)) + .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(participantId), keyId)) .then() .log().ifValidationFails() .statusCode(204) .body(notNullValue()); - assertThat(context.getDidForParticipant(user1)).hasSize(1) + assertThat(context.getDidForParticipant(participantId)).hasSize(1) .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyId))); + + // verify that the STS client got updated correctly + assertThat(clientStore.findById(participantId)).isSucceeded() + .satisfies(stsClient -> { + assertThat(stsClient.getPrivateKeyAlias()).isEqualTo("new-alias"); + assertThat(stsClient.getPublicKeyReference()).isEqualTo("did:web:" + participantId + "#new-keyId"); + }); + }); + } + + @Test + void revoke_withoutNewKey(IdentityHubEndToEndTestContext context, EventRouter router, StsClientStore clientStore) { + var subscriber = mock(EventSubscriber.class); + router.registerSync(KeyPairRotated.class, subscriber); + router.registerSync(KeyPairRevoked.class, subscriber); + + var participantId = "user1"; + var userToken = context.createParticipant(participantId); + + var keyPairId = context.createKeyPair(participantId).getResourceId(); + + // attempt to publish user1's DID document, which should fail + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", userToken)) + .post("/v1alpha/participants/%s/keypairs/%s/revoke".formatted(toBase64(participantId), keyPairId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + // verify that the "rotated" event fired once + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairRevoked evt) { + return evt.getParticipantId().equals(participantId); + } + return false; + })); + // verify that the correct "added" event fired + verify(subscriber, never()).on(argThat(env -> env.getPayload() instanceof KeyPairAdded)); + + // verify that the STS client got updated correctly + assertThat(clientStore.findById(participantId)).isSucceeded() + .satisfies(stsClient -> { + assertThat(stsClient.getPrivateKeyAlias()).isEqualTo(""); + assertThat(stsClient.getPublicKeyReference()).isEqualTo(""); }); } @@ -668,7 +781,7 @@ void activate_superUserToken(IdentityHubEndToEndTestContext context, EventRouter .anySatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(2).anyMatch(vm -> vm.getId().equals(keyDescriptor.getKeyId()))); assertThat(context.getDidResourceForParticipant("did:web:" + user1).getState()).isEqualTo(DidState.PUBLISHED.code()); - verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResourceId().equals(keyPairId))); + verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResource().getId().equals(keyPairId))); } @Test @@ -702,7 +815,7 @@ void activate_userToken(IdentityHubEndToEndTestContext context, EventRouter rout .anyMatch(vm -> vm.getId().equals(keyDescriptor.getKeyId()))); assertThat(context.getDidResourceForParticipant("did:web:" + participantId).getState()).isEqualTo(DidState.PUBLISHED.code()); - verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResourceId().equals(keyPairId))); + verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResource().getId().equals(keyPairId))); // publishes did when creating the user, and when activating verify(subscriber, atLeast(2)).on(argThat(e -> e.getPayload() instanceof DidDocumentPublished)); } @@ -738,7 +851,7 @@ void activate_whenParticipantNotActive_shouldNotPublishDid(IdentityHubEndToEndTe assertThat(context.getKeyPairsForParticipant(user1)) .allMatch(kpr -> kpr.getState() == KeyPairState.ACTIVATED.code()); - verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResourceId().equals(keyPairId))); + verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResource().getId().equals(keyPairId))); } @Test diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java index 2bc105583..a611f6794 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java @@ -17,6 +17,7 @@ import io.restassured.http.ContentType; import io.restassured.http.Header; import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; import org.eclipse.edc.identithub.spi.did.DidConstants; import org.eclipse.edc.identithub.spi.did.DidDocumentPublisher; import org.eclipse.edc.identithub.spi.did.DidDocumentPublisherRegistry; @@ -76,7 +77,7 @@ public class ParticipantContextApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, StsClientStore stsClientStore) { // purge all users, dids, keypairs pcService.query(QuerySpec.max()).getContent() @@ -86,6 +87,9 @@ void tearDown(ParticipantContextService pcService, DidResourceStore didResourceS keyPairResourceStore.query(QuerySpec.max()).getContent() .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); + + stsClientStore.findAll(QuerySpec.max()) + .forEach(sts -> stsClientStore.deleteById(sts.getId()).getContent()); } @Test @@ -145,7 +149,9 @@ void createNewUser_principalIsSuperuser(IdentityHubEndToEndTestContext context, .then() .log().ifError() .statusCode(anyOf(equalTo(200), equalTo(204))) - .body(notNullValue()); + .body("clientId", notNullValue()) + .body("apiKey", notNullValue()) + .body("clientSecret", notNullValue()); verify(subscriber).on(argThat(env -> ((ParticipantContextCreated) env.getPayload()).getParticipantId().equals(manifest.getParticipantId()))); 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 a73686219..ad43a97e7 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 @@ -26,6 +26,7 @@ import jakarta.json.JsonString; import jakarta.json.JsonValue; import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; import org.eclipse.edc.iam.verifiablecredentials.spi.model.RevocationServiceRegistry; @@ -131,7 +132,7 @@ void setup(IdentityHubEndToEndTestContext context) { } @AfterEach - void teardown(ParticipantContextService contextService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, CredentialStore store) { + void teardown(ParticipantContextService contextService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, CredentialStore store, StsClientStore stsClientStore) { // purge all participant contexts contextService.query(QuerySpec.max()).getContent() @@ -146,6 +147,9 @@ void teardown(ParticipantContextService contextService, DidResourceStore didReso store.query(QuerySpec.none()) .map(creds -> creds.stream().map(cred -> store.deleteById(cred.getId())).toList()) .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + + stsClientStore.findAll(QuerySpec.max()) + .forEach(sts -> stsClientStore.deleteById(sts.getId()).getContent()); } @Test @@ -312,7 +316,7 @@ void query_success_noCredentials(IdentityHubEndToEndTestContext context) throws assertThat(response) .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(1)) .hasEntrySatisfying("presentation", jsonValue -> assertThat(extractCredentials(((JsonString) jsonValue).getString())).isEmpty()); } @@ -346,7 +350,7 @@ void query_success_containsCredential(IdentityHubEndToEndTestContext context, Cr assertThat(response) .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(1)) .hasEntrySatisfying("presentation", jsonValue -> { assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); var vpToken = ((JsonString) jsonValue).getString(); @@ -407,7 +411,7 @@ void query_shouldFilterOutInvalidCreds(int vcStateCode, IdentityHubEndToEndTestC assertThat(response) .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) - .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(1)) .hasEntrySatisfying("presentation", jsonValue -> { assertThat(jsonValue.getValueType()).isEqualTo(JsonValue.ValueType.STRING); var vpToken = ((JsonString) jsonValue).getString(); 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 d95051438..2b7eac339 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 @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.tests; import io.restassured.http.Header; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; 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; @@ -46,7 +47,7 @@ public class VerifiableCredentialApiEndToEndTest { abstract static class Tests { @AfterEach - void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore) { + void tearDown(ParticipantContextService pcService, DidResourceStore didResourceStore, KeyPairResourceStore keyPairResourceStore, StsClientStore stsClientStore) { // purge all users, dids, keypairs pcService.query(QuerySpec.max()).getContent() @@ -56,6 +57,10 @@ void tearDown(ParticipantContextService pcService, DidResourceStore didResourceS keyPairResourceStore.query(QuerySpec.max()).getContent() .forEach(kpr -> keyPairResourceStore.deleteById(kpr.getId()).getContent()); + + stsClientStore.findAll(QuerySpec.max()) + .forEach(sts -> stsClientStore.deleteById(sts.getId()).getContent()); + } @Test diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java index a05d7eece..7e6cd6335 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java @@ -89,7 +89,7 @@ public String createParticipant(String participantId, List roles, boolea .build()) .build(); var srv = runtime.getService(ParticipantContextService.class); - return srv.createParticipantContext(manifest).orElseThrow(f -> new EdcException(f.getFailureDetail())); + return srv.createParticipantContext(manifest).orElseThrow(f -> new EdcException(f.getFailureDetail())).get("apiKey").toString(); } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java index 3be2d7173..d28c81359 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubRuntimeConfiguration.java @@ -48,6 +48,8 @@ public Map config() { put("web.http.presentation.path", presentationEndpoint.getUrl().getPath()); put("web.http.identity.port", String.valueOf(identityEndpoint.getUrl().getPort())); put("web.http.identity.path", identityEndpoint.getUrl().getPath()); + put("web.http.sts.port", String.valueOf(getFreePort())); + put("web.http.sts.path", "/api/sts"); put("edc.runtime.id", name); put("edc.ih.iam.id", "did:web:consumer"); put("edc.sql.schema.autocreate", "true"); diff --git a/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApi.java b/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApi.java index f087aedeb..8b0675f48 100644 --- a/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApi.java +++ b/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApi.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; @OpenAPIDefinition(info = @Info(description = "This is the Identity API for manipulating ParticipantContexts", title = "ParticipantContext Management API", version = "1")) @Tag(name = "Participant Context") @@ -50,7 +51,7 @@ public interface ParticipantContextApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - String createParticipant(ParticipantManifest manifest); + Map createParticipant(ParticipantManifest manifest); @Operation(description = "Gets ParticipantContexts by ID.", diff --git a/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApiController.java b/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApiController.java index 270837012..988fb8490 100644 --- a/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApiController.java +++ b/extensions/api/identity-api/participant-context-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/unstable/ParticipantContextApiController.java @@ -40,6 +40,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @@ -63,7 +64,7 @@ public ParticipantContextApiController(ParticipantManifestValidator participantM @Override @POST @RolesAllowed(ServicePrincipal.ROLE_ADMIN) - public String createParticipant(ParticipantManifest manifest) { + public Map createParticipant(ParticipantManifest manifest) { participantManifestValidator.validate(manifest).orElseThrow(ValidationFailureException::new); return participantContextService.createParticipantContext(manifest) .orElseThrow(exceptionMapper(ParticipantManifest.class, manifest.getParticipantId())); diff --git a/extensions/common/sts-account-provisioner/build.gradle.kts b/extensions/common/sts-account-provisioner/build.gradle.kts new file mode 100644 index 000000000..c020e2260 --- /dev/null +++ b/extensions/common/sts-account-provisioner/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 + * + */ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + + implementation(libs.edc.sts.spi) + implementation(libs.edc.spi.core) + implementation(libs.edc.spi.transaction) + implementation(project(":spi:participant-context-spi")) + implementation(project(":spi:keypair-spi")) + implementation(project(":spi:did-spi")) + testImplementation(libs.edc.junit) +} diff --git a/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisioner.java b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisioner.java new file mode 100644 index 000000000..02879723d --- /dev/null +++ b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisioner.java @@ -0,0 +1,160 @@ +/* + * 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.common.provisioner; + +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountInfo; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountProvisioner; +import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; +import org.eclipse.edc.spi.event.Event; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventSubscriber; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * AccountProvisioner, that synchronizes the {@link org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext} object + * to {@link StsClient} entries. That means, when a participant is created, this provisioner takes care of creating a corresponding + * {@link StsClient}, if the embedded STS is used. + * When key pairs are revoked or rotated, the corresponding {@link StsClient} entry is updated. + */ +public class StsAccountProvisioner implements EventSubscriber, AccountProvisioner { + + private final Monitor monitor; + private final StsClientStore stsClientStore; + private final Vault vault; + private final StsClientSecretGenerator stsClientSecretGenerator; + private final TransactionContext transactionContext; + + public StsAccountProvisioner(Monitor monitor, + StsClientStore stsClientStore, + Vault vault, + StsClientSecretGenerator stsClientSecretGenerator, + TransactionContext transactionContext) { + this.monitor = monitor; + this.stsClientStore = stsClientStore; + this.vault = vault; + this.stsClientSecretGenerator = stsClientSecretGenerator; + this.transactionContext = transactionContext; + } + + @Override + public void on(EventEnvelope event) { + var payload = event.getPayload(); + ServiceResult result; + if (payload instanceof ParticipantContextDeleted deletedEvent) { + result = deleteAccount(deletedEvent.getParticipantId()); + } else if (payload instanceof KeyPairRevoked kpe) { + result = updateStsClient(kpe.getKeyPairResource(), kpe.getParticipantId(), kpe.getNewKeyDescriptor()); + } else if (payload instanceof KeyPairRotated kpr) { + result = updateStsClient(kpr.getKeyPairResource(), kpr.getParticipantId(), kpr.getNewKeyDescriptor()); + } else { + result = ServiceResult.badRequest("Received event with unexpected payload type: %s".formatted(payload.getClass())); + } + + result.onFailure(f -> monitor.warning(f.getFailureDetail())); + } + + @Override + public ServiceResult create(ParticipantManifest manifest) { + return transactionContext.execute(() -> { + var secretAlias = manifest.getParticipantId() + "-sts-client-secret"; + + var client = StsClient.Builder.newInstance() + .id(manifest.getParticipantId()) + .name(manifest.getParticipantId()) + .clientId(manifest.getDid()) + .did(manifest.getDid()) + .privateKeyAlias(manifest.getKey().getPrivateKeyAlias()) + .publicKeyReference(manifest.getKey().getKeyId()) + .secretAlias(secretAlias) + .build(); + + var createResult = stsClientStore.create(client) + .map(stsClient -> { + var clientSecret = stsClientSecretGenerator.generateClientSecret(null); + return new AccountInfo(stsClient.getClientId(), clientSecret); + }) + .onSuccess(accountInfo -> { + // the vault's result does not influence the service result, since that may cause the transaction to roll back, + // but vaults aren't transactional resources + vault.storeSecret(secretAlias, accountInfo.clientSecret()) + .onFailure(e -> monitor.severe(e.getFailureDetail())); + }); + + return createResult.succeeded() ? ServiceResult.success(createResult.getContent()) : ServiceResult.badRequest(createResult.getFailureDetail()); + }); + } + + + private ServiceResult updateStsClient(KeyPairResource oldKeyResource, String participantId, @Nullable KeyDescriptor newKeyDescriptor) { + return transactionContext.execute(() -> { + var findResult = stsClientStore.findById(participantId); + if (findResult.failed()) { + return ServiceResult.from(findResult).mapEmpty(); + } + + var existingClient = findResult.getContent(); + + if (Objects.equals(oldKeyResource.getPrivateKeyAlias(), existingClient.getPrivateKeyAlias())) { + return ServiceResult.success(); // the revoked/rotated key pair does not pertain to this STS Client + } + + if (newKeyDescriptor == null) { + // no "successor" key was given, will only reset + return setKeyAliases(existingClient, "", ""); + } + + var publicKeyRef = newKeyDescriptor.getKeyId(); + // check that key-id contains the DID + if (!publicKeyRef.startsWith(existingClient.getDid())) { + publicKeyRef = existingClient.getDid() + "#" + publicKeyRef; + } + return setKeyAliases(existingClient, newKeyDescriptor.getPrivateKeyAlias(), publicKeyRef); + }); + } + + private ServiceResult deleteAccount(String participantId) { + var result = transactionContext.execute(() -> stsClientStore.deleteById(participantId)); + return ServiceResult.from(result).mapEmpty(); + } + + private ServiceResult setKeyAliases(StsClient stsClient, String privateKeyAlias, String publicKeyReference) { + var updatedClient = transactionContext.execute(() -> { + var newClient = StsClient.Builder.newInstance() + .id(stsClient.getId()) + .clientId(stsClient.getClientId()) + .did(stsClient.getDid()) + .name(stsClient.getName()) + .secretAlias(stsClient.getSecretAlias()) + .privateKeyAlias(privateKeyAlias) + .publicKeyReference(publicKeyReference) + .build(); + return stsClientStore.update(newClient); + }); + return ServiceResult.from(updatedClient); + } +} diff --git a/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerExtension.java b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerExtension.java new file mode 100644 index 000000000..acf29a689 --- /dev/null +++ b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerExtension.java @@ -0,0 +1,114 @@ +/* + * 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.common.provisioner; + +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.identithub.spi.did.DidDocumentService; +import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; +import org.eclipse.edc.identityhub.spi.participantcontext.AccountProvisioner; +import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted; +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 org.jetbrains.annotations.Nullable; + +import java.security.SecureRandom; + +import static java.util.Optional.ofNullable; +import static org.eclipse.edc.identityhub.common.provisioner.StsAccountProvisionerExtension.NAME; + +@Extension(value = NAME) +public class StsAccountProvisionerExtension implements ServiceExtension { + public static final String NAME = "STS Account Provisioner Extension"; + public static final int DEFAULT_CLIENT_SECRET_LENGTH = 16; + @Inject + private EventRouter eventRouter; + @Inject + private KeyPairService keyPairService; + @Inject + private DidDocumentService didDocumentService; + @Inject(required = false) + private StsClientStore stsClientStore; + @Inject + private Vault vault; + @Inject(required = false) + private StsClientSecretGenerator stsClientSecretGenerator; + @Inject + private TransactionContext transactionContext; + + private StsAccountProvisioner provisioner; + + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + // invoke once, so that the event registration definitely happens + createProvisioner(context); + } + + @Provider + public AccountProvisioner createProvisioner(ServiceExtensionContext context) { + if (provisioner == null) { + var monitor = context.getMonitor().withPrefix("STS-Account"); + if (stsClientStore != null) { + monitor.info("This IdentityHub runtime contains an embedded SecureTokenService (STS) instance. That means ParticipantContexts and STS Accounts will be synchronized automatically."); + provisioner = new StsAccountProvisioner(monitor, stsClientStore, vault, stsClientSecretGenerator(), transactionContext); + eventRouter.registerSync(ParticipantContextDeleted.class, provisioner); + eventRouter.registerSync(KeyPairRevoked.class, provisioner); + eventRouter.registerSync(KeyPairRotated.class, provisioner); + } else { + monitor.warning("This IdentityHub runtime does NOT contain an embedded SecureTokenService (STS) instance. " + + "Synchronizing ParticipantContexts and STS Accounts must be handled out-of-band."); + } + } + return provisioner; + } + + private StsClientSecretGenerator stsClientSecretGenerator() { + return ofNullable(stsClientSecretGenerator) + .orElseGet(RandomStringGenerator::new); + } + + /** + * Default client secret generator that creates an alpha-numeric string of length {@link StsAccountProvisionerExtension#DEFAULT_CLIENT_SECRET_LENGTH} + * (16). + */ + private static class RandomStringGenerator implements StsClientSecretGenerator { + @Override + public String generateClientSecret(@Nullable Object parameters) { + // algorithm taken from https://www.baeldung.com/java-random-string + int leftLimit = 48; // numeral '0' + int rightLimit = 122; // letter 'z' + var random = new SecureRandom(); + + return random.ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(DEFAULT_CLIENT_SECRET_LENGTH) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + } +} diff --git a/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsClientSecretGenerator.java b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsClientSecretGenerator.java new file mode 100644 index 000000000..dda700402 --- /dev/null +++ b/extensions/common/sts-account-provisioner/src/main/java/org/eclipse/edc/identityhub/common/provisioner/StsClientSecretGenerator.java @@ -0,0 +1,30 @@ +/* + * 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.common.provisioner; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.jetbrains.annotations.Nullable; + +@ExtensionPoint +@FunctionalInterface +public interface StsClientSecretGenerator { + /** + * Generates a client secret as string, taking an optional argument. By default, + * + * @param parameters Optional generator arguments, such as a salt value + * @return a randomly generated client secret. + */ + String generateClientSecret(@Nullable Object parameters); +} diff --git a/extensions/common/sts-account-provisioner/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/common/sts-account-provisioner/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..866289f4f --- /dev/null +++ b/extensions/common/sts-account-provisioner/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# 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 +# +# + +org.eclipse.edc.identityhub.common.provisioner.StsAccountProvisionerExtension \ No newline at end of file diff --git a/extensions/common/sts-account-provisioner/src/test/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerTest.java b/extensions/common/sts-account-provisioner/src/test/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerTest.java new file mode 100644 index 000000000..fab8d2b1c --- /dev/null +++ b/extensions/common/sts-account-provisioner/src/test/java/org/eclipse/edc/identityhub/common/provisioner/StsAccountProvisionerTest.java @@ -0,0 +1,175 @@ +/* + * 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.common.provisioner; + +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.identithub.spi.did.DidDocumentService; +import org.eclipse.edc.identityhub.spi.keypair.KeyPairService; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked; +import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRotated; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; +import org.eclipse.edc.spi.event.Event; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.transaction.spi.NoopTransactionContext; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.UUID; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +class StsAccountProvisionerTest { + + private static final String PARTICIPANT_CONTEXT_ID = "test-participant"; + private static final String PARTICIPANT_DID = "did:web:" + PARTICIPANT_CONTEXT_ID; + private static final String KEY_ID = "test-key-id"; + private final KeyPairService keyPairService = mock(); + private final DidDocumentService didDocumentService = mock(); + private final StsClientStore stsClientStore = mock(); + private final Vault vault = mock(); + private final Monitor monitor = mock(); + private final StsClientSecretGenerator stsClientSecretGenerator = parameters -> UUID.randomUUID().toString(); + private final StsAccountProvisioner accountProvisioner = new StsAccountProvisioner(monitor, stsClientStore, vault, stsClientSecretGenerator, new NoopTransactionContext()); + + @Test + void create() { + when(stsClientStore.create(any())).thenReturn(StoreResult.success(createStsClient().build())); + when(vault.storeSecret(anyString(), anyString())).thenReturn(Result.success()); + + assertThat(accountProvisioner.create(createManifest().build())).isSucceeded(); + + verify(stsClientStore).create(any()); + verify(vault).storeSecret(anyString(), argThat(secret -> UUID.fromString(secret) != null)); + verifyNoInteractions(keyPairService, didDocumentService); + } + + @Test + void create_whenClientAlreadyExists() { + when(stsClientStore.create(any())).thenReturn(StoreResult.alreadyExists("foo")); + + var res = accountProvisioner.create(createManifest().build()); + assertThat(res).isFailed() + .detail().isEqualTo("foo"); + + verify(stsClientStore).create(any()); + verifyNoInteractions(keyPairService, didDocumentService, vault); + } + + @Test + void onKeyRevoked_shouldUpdate() { + when(stsClientStore.findById(PARTICIPANT_CONTEXT_ID)).thenReturn(StoreResult.success(createStsClient().build())); + when(stsClientStore.update(any())).thenAnswer(a -> StoreResult.success(a.getArguments()[0])); + accountProvisioner.on(event(KeyPairRevoked.Builder.newInstance() + .participantId(PARTICIPANT_CONTEXT_ID) + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) + .keyId(KEY_ID) + .build())); + + verify(stsClientStore).findById(PARTICIPANT_CONTEXT_ID); + verify(stsClientStore).update(any()); + verifyNoMoreInteractions(stsClientStore, didDocumentService, keyPairService); + } + + @Test + void onKeyRotated_withNewKey_shouldUpdate() { + when(stsClientStore.findById(PARTICIPANT_CONTEXT_ID)).thenReturn(StoreResult.success(createStsClient().build())); + when(stsClientStore.update(any())).thenAnswer(a -> StoreResult.success(a.getArguments()[0])); + + accountProvisioner.on(event(KeyPairRotated.Builder.newInstance() + .participantId(PARTICIPANT_CONTEXT_ID) + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) + .keyId(KEY_ID) + .build())); + + verify(stsClientStore).findById(PARTICIPANT_CONTEXT_ID); + verify(stsClientStore).update(any()); + verifyNoMoreInteractions(stsClientStore, didDocumentService, keyPairService); + } + + @Test + void onParticipantDeleted_shouldDelete() { + when(stsClientStore.deleteById(PARTICIPANT_CONTEXT_ID)).thenReturn(StoreResult.success()); + accountProvisioner.on(event(ParticipantContextDeleted.Builder.newInstance() + .participantId(PARTICIPANT_CONTEXT_ID) + .build())); + + verify(stsClientStore).deleteById(PARTICIPANT_CONTEXT_ID); + verifyNoMoreInteractions(keyPairService, didDocumentService, stsClientStore); + } + + @Test + void onOtherEvent_shouldLogWarning() { + accountProvisioner.on(event(new DummyEvent())); + verify(monitor).warning(startsWith("Received event with unexpected payload")); + verifyNoInteractions(keyPairService, didDocumentService, stsClientStore, vault); + } + + private StsClient.Builder createStsClient() { + return StsClient.Builder.newInstance() + .id("test-id") + .name("test-name") + .did("did:web:" + PARTICIPANT_CONTEXT_ID) + .secretAlias("test-secret") + .publicKeyReference("public-key-ref") + .privateKeyAlias("private-key-alias") + .clientId("client-id"); + } + + private ParticipantManifest.Builder createManifest() { + return ParticipantManifest.Builder.newInstance() + .participantId(PARTICIPANT_CONTEXT_ID) + .active(true) + .did(PARTICIPANT_DID) + .key(KeyDescriptor.Builder.newInstance() + .privateKeyAlias(KEY_ID + "-alias") + .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) + .keyId(KEY_ID) + .build() + ); + } + + @SuppressWarnings("unchecked") + private EventEnvelope event(Event event) { + return EventEnvelope.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .at(System.currentTimeMillis()) + .payload(event) + .build(); + } + + private static class DummyEvent extends Event { + @Override + public String name() { + return "dummy"; + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44f5798a4..a7bcadca5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,11 @@ edc-lib-json = { module = "org.eclipse.edc:json-lib", version.ref = "edc" } edc-lib-common-crypto = { module = "org.eclipse.edc:crypto-common-lib", version.ref = "edc" } edc-core-jerseyproviders = { module = "org.eclipse.edc:jersey-providers-lib", version.ref = "edc" } +# EDC STS dependencies +edc-sts-spi = { module = "org.eclipse.edc:identity-trust-sts-spi", version.ref = "edc" } +edc-sts-core = { module = "org.eclipse.edc:identity-trust-sts-core", version.ref = "edc" } +edc-sts = { module = "org.eclipse.edc:identity-trust-sts-embedded", version.ref = "edc" } +edc-sts-api = { module = "org.eclipse.edc:identity-trust-sts-api", version.ref = "edc" } # Third party libs assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } diff --git a/launcher/build.gradle.kts b/launcher/build.gradle.kts index b0b058a49..cb7b761c2 100644 --- a/launcher/build.gradle.kts +++ b/launcher/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { runtimeOnly(project(":core:identity-hub-keypairs")) runtimeOnly(project(":extensions:did:local-did-publisher")) runtimeOnly(project(":extensions:common:credential-watchdog")) + runtimeOnly(project(":extensions:common:sts-account-provisioner")) runtimeOnly(project(":extensions:api:identity-api:did-api")) runtimeOnly(project(":extensions:api:identity-api:participant-context-api")) runtimeOnly(project(":extensions:api:identity-api:verifiable-credentials-api")) @@ -36,6 +37,9 @@ dependencies { runtimeOnly(libs.edc.identity.did.core) runtimeOnly(libs.edc.core.token) runtimeOnly(libs.edc.api.version) + runtimeOnly(libs.edc.sts.core) + runtimeOnly(libs.edc.sts) + runtimeOnly(libs.edc.sts.api) runtimeOnly(libs.edc.identity.did.web) runtimeOnly(libs.bundles.connector) diff --git a/settings.gradle.kts b/settings.gradle.kts index 85d7057f8..e6d20d645 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,6 +50,7 @@ include(":extensions:store:sql:identity-hub-participantcontext-store-sql") include(":extensions:store:sql:identity-hub-keypair-store-sql") include(":extensions:did:local-did-publisher") include(":extensions:common:credential-watchdog") +include(":extensions:common:sts-account-provisioner") // Identity APIs include(":extensions:api:identity-api:validators") diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java index 085bb72f1..c6bd50e41 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/ParticipantContextStore.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.spi.store; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; @@ -63,4 +64,14 @@ default String alreadyExistsErrorMessage(String id) { default String notFoundErrorMessage(String id) { return "A ParticipantContext with ID '%s' does not exist.".formatted(id); } + + default StoreResult findById(String participantId) { + var res = query(ParticipantResource.queryByParticipantId(participantId).build()); + if (res.succeeded()) { + return res.getContent().stream().findFirst() + .map(StoreResult::success) + .orElse(StoreResult.notFound("ParticipantContext with ID '%s' does not exist.".formatted(participantId))); + } + return StoreResult.generalError(res.getFailureDetail()); + } } diff --git a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEvent.java b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEvent.java index 256e3998a..0c8447bf3 100644 --- a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEvent.java +++ b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEvent.java @@ -25,14 +25,14 @@ */ public abstract class KeyPairEvent extends Event { protected String participantId; - protected String keyPairResourceId; + protected KeyPairResource keyPairResource; protected String keyId; /** - * The ID of the {@link KeyPairResource}. This is the internal database ID. + * The {@link KeyPairResource} that this event refers to. */ - public String getKeyPairResourceId() { - return keyPairResourceId; + public KeyPairResource getKeyPairResource() { + return keyPairResource; } /** @@ -69,8 +69,8 @@ public B keyId(String keyId) { return self(); } - public B keyPairResourceId(String keyPairResourceId) { - event.keyPairResourceId = keyPairResourceId; + public B keyPairResource(KeyPairResource keyPairResource) { + event.keyPairResource = keyPairResource; return self(); } diff --git a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEventListener.java b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEventListener.java index ccef78f74..d1c2ad2d5 100644 --- a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEventListener.java +++ b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairEventListener.java @@ -15,8 +15,10 @@ package org.eclipse.edc.identityhub.spi.keypair.events; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.spi.observe.Observable; +import org.jetbrains.annotations.Nullable; /** * Interface implemented by listeners registered to observe key pair resource changes via {@link Observable#registerListener}. @@ -39,9 +41,10 @@ default void added(KeyPairResource keypair, String type) { * A {@link KeyPairResource} was rotated (=phased out). If the rotation was done with a successor keypair, this would be communicated using the {@link KeyPairEventListener#added(KeyPairResource, String)} * callback. * - * @param keyPair the old (outgoing) {@link KeyPairResource} + * @param keyPair the old (outgoing) {@link KeyPairResource} + * @param newKeyDesc The {@link KeyDescriptor} of the new Key pair. can be null. */ - default void rotated(KeyPairResource keyPair) { + default void rotated(KeyPairResource keyPair, @Nullable KeyDescriptor newKeyDesc) { } @@ -49,9 +52,10 @@ default void rotated(KeyPairResource keyPair) { * A {@link KeyPairResource} was revoked (=deleted). If the revocation was done with a successor keypair, this would be communicated using the {@link KeyPairEventListener#added(KeyPairResource, String)} * callback. * - * @param keyPair the old (outgoing) {@link KeyPairResource} + * @param keyPair the old (outgoing) {@link KeyPairResource} + * @param newKeyDesc The {@link KeyDescriptor} of the new Key pair. can be null. */ - default void revoked(KeyPairResource keyPair) { + default void revoked(KeyPairResource keyPair, @Nullable KeyDescriptor newKeyDesc) { } diff --git a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevoked.java b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevoked.java index 3612551a3..7fc2e8974 100644 --- a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevoked.java +++ b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevoked.java @@ -17,17 +17,25 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; +import org.jetbrains.annotations.Nullable; /** * Event that signals that a KeyPair was revoked. */ @JsonDeserialize(builder = KeyPairRevoked.Builder.class) public class KeyPairRevoked extends KeyPairEvent { + private @Nullable KeyDescriptor newKeyDescriptor; + @Override public String name() { return "keypair.revoked"; } + public @Nullable KeyDescriptor getNewKeyDescriptor() { + return newKeyDescriptor; + } + @JsonPOJOBuilder(withPrefix = "") public static class Builder extends KeyPairEvent.Builder { @@ -44,5 +52,10 @@ public static KeyPairRevoked.Builder newInstance() { public KeyPairRevoked.Builder self() { return this; } + + public Builder newKeyDescriptor(@Nullable KeyDescriptor newKeyDescriptor) { + event.newKeyDescriptor = newKeyDescriptor; + return this; + } } } diff --git a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotated.java b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotated.java index 7b28ba4d2..c83b66fbc 100644 --- a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotated.java +++ b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotated.java @@ -17,17 +17,25 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; +import org.jetbrains.annotations.Nullable; /** * Event that signals that a KeyPair was rotated. */ @JsonDeserialize(builder = KeyPairRotated.Builder.class) public class KeyPairRotated extends KeyPairEvent { + private @Nullable KeyDescriptor newKeyDescriptor; + @Override public String name() { return "keypair.rotated"; } + public @Nullable KeyDescriptor getNewKeyDescriptor() { + return newKeyDescriptor; + } + @JsonPOJOBuilder(withPrefix = "") public static class Builder extends KeyPairEvent.Builder { @@ -44,5 +52,10 @@ public static KeyPairRotated.Builder newInstance() { public KeyPairRotated.Builder self() { return this; } + + public Builder newKeyDescriptor(@Nullable KeyDescriptor newKeyDesc) { + event.newKeyDescriptor = newKeyDesc; + return this; + } } } diff --git a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/model/KeyPairResource.java b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/model/KeyPairResource.java index 9452ece90..1454e36ec 100644 --- a/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/model/KeyPairResource.java +++ b/spi/keypair-spi/src/main/java/org/eclipse/edc/identityhub/spi/keypair/model/KeyPairResource.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.security.Vault; import java.time.Duration; @@ -42,6 +44,10 @@ public class KeyPairResource extends ParticipantResource { private KeyPairResource() { } + public static QuerySpec.Builder queryById(String id) { + return QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", id)); + } + public String getGroupName() { return groupName; } diff --git a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairAddedTest.java b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairAddedTest.java index e35b8add4..22923895f 100644 --- a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairAddedTest.java +++ b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairAddedTest.java @@ -14,10 +14,13 @@ package org.eclipse.edc.identityhub.spi.keypair.events; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.json.JacksonTypeManager; import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.Test; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; class KeyPairAddedTest { @@ -27,7 +30,7 @@ class KeyPairAddedTest { @Test void verify_serDes() { var evt = KeyPairAdded.Builder.newInstance() - .keyPairResourceId("resource-id") + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) .keyId("key-id") .participantId("participant-id") .build(); diff --git a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevokedTest.java b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevokedTest.java index f4f1de2a8..4141172bc 100644 --- a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevokedTest.java +++ b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRevokedTest.java @@ -14,10 +14,13 @@ package org.eclipse.edc.identityhub.spi.keypair.events; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.json.JacksonTypeManager; import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.Test; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; class KeyPairRevokedTest { @@ -27,7 +30,7 @@ class KeyPairRevokedTest { @Test void verify_serDes() { var evt = KeyPairRevoked.Builder.newInstance() - .keyPairResourceId("resource-id") + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) .keyId("key-id") .participantId("participant-id") .build(); diff --git a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotatedTest.java b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotatedTest.java index dc966550f..85df2c99c 100644 --- a/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotatedTest.java +++ b/spi/keypair-spi/src/test/java/org/eclipse/edc/identityhub/spi/keypair/events/KeyPairRotatedTest.java @@ -14,10 +14,13 @@ package org.eclipse.edc.identityhub.spi.keypair.events; +import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.json.JacksonTypeManager; import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.Test; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; class KeyPairRotatedTest { @@ -27,7 +30,7 @@ class KeyPairRotatedTest { @Test void verify_serDes() { var evt = KeyPairRotated.Builder.newInstance() - .keyPairResourceId("resource-id") + .keyPairResource(KeyPairResource.Builder.newInstance().id(UUID.randomUUID().toString()).build()) .keyId("key-id") .participantId("participant-id") .build(); diff --git a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountInfo.java b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountInfo.java new file mode 100644 index 000000000..ef7029af7 --- /dev/null +++ b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountInfo.java @@ -0,0 +1,18 @@ +/* + * 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.spi.participantcontext; + +public record AccountInfo(String clientId, String clientSecret) { +} diff --git a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountProvisioner.java b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountProvisioner.java new file mode 100644 index 000000000..d5d1c0253 --- /dev/null +++ b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/AccountProvisioner.java @@ -0,0 +1,22 @@ +/* + * 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.spi.participantcontext; + +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; +import org.eclipse.edc.spi.result.ServiceResult; + +public interface AccountProvisioner { + ServiceResult create(ParticipantManifest manifest); +} diff --git a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/ParticipantContextService.java b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/ParticipantContextService.java index 9d2827609..143f2e087 100644 --- a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/ParticipantContextService.java +++ b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/ParticipantContextService.java @@ -20,6 +20,7 @@ import org.eclipse.edc.spi.result.ServiceResult; import java.util.Collection; +import java.util.Map; import java.util.function.Consumer; /** @@ -34,7 +35,7 @@ public interface ParticipantContextService { * @param manifest The new participant context * @return success if created, or a failure if already exists. */ - ServiceResult createParticipantContext(ParticipantManifest manifest); + ServiceResult> createParticipantContext(ParticipantManifest manifest); /** * Fetches the {@link ParticipantContext} by ID.