Skip to content

Commit

Permalink
chore: align DID operations with DR (#450)
Browse files Browse the repository at this point in the history
* chore: align DID operations with DR

* checkstyle

* fix test, deactivate PC before delete
  • Loading branch information
paullatzelsperger authored Sep 10, 2024
1 parent a8e4b5f commit f906ff9
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 94 deletions.
1 change: 1 addition & 0 deletions core/identity-hub-did/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
api(project(":spi:did-spi"))

implementation(project(":spi:keypair-spi"))
implementation(project(":spi:identity-hub-store-spi"))
implementation(project(":spi:participant-context-spi"))
implementation(libs.edc.core.connector) // for the reflection-based query resolver
implementation(libs.edc.lib.common.crypto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import org.eclipse.edc.identithub.spi.did.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
import org.eclipse.edc.security.token.jwt.CryptoConverter;
Expand Down Expand Up @@ -54,13 +57,16 @@ public class DidDocumentServiceImpl implements DidDocumentService, EventSubscrib
private final TransactionContext transactionContext;
private final DidResourceStore didResourceStore;
private final DidDocumentPublisherRegistry registry;
private final ParticipantContextService participantContextService;
private final Monitor monitor;
private final KeyParserRegistry keyParserRegistry;

public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry, Monitor monitor, KeyParserRegistry keyParserRegistry) {
public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry,
ParticipantContextService participantContextService, Monitor monitor, KeyParserRegistry keyParserRegistry) {
this.transactionContext = transactionContext;
this.didResourceStore = didResourceStore;
this.registry = registry;
this.participantContextService = participantContextService;
this.monitor = monitor;
this.keyParserRegistry = keyParserRegistry;
}
Expand Down Expand Up @@ -101,42 +107,66 @@ public ServiceResult<Void> deleteById(String did) {
@Override
public ServiceResult<Void> publish(String did) {
return transactionContext.execute(() -> {
var existingDoc = didResourceStore.findById(did);
if (existingDoc == null) {
var existingResource = didResourceStore.findById(did);
if (existingResource == null) {
return ServiceResult.notFound(notFoundMessage(did));
}
var publisher = registry.getPublisher(did);
if (publisher == null) {
return ServiceResult.badRequest(noPublisherFoundMessage(did));
}
var publishResult = publisher.publish(did);
return publishResult.succeeded() ?
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());
var participantId = existingResource.getParticipantId();
return participantContextService.getParticipantContext(participantId)
.map(ParticipantContext::getStateAsEnum)
.compose(state -> {
var canPublish = state.equals(ParticipantContextState.ACTIVATED);
if (canPublish) {
var publisher = registry.getPublisher(did);
if (publisher == null) {
return ServiceResult.badRequest(noPublisherFoundMessage(did));
}
var publishResult = publisher.publish(did);
return publishResult.succeeded() ?
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());
}
return ServiceResult.badRequest(("Cannot publish DID '%s' for participant '%s' because the ParticipantContext is not state '%s' state, " +
"but was '%s'.")
.formatted(did, participantId, ParticipantContextState.ACTIVATED, state));
});


});
}

@Override
public ServiceResult<Void> unpublish(String did) {
return transactionContext.execute(() -> {
var existingDoc = didResourceStore.findById(did);
if (existingDoc == null) {
var existingResource = didResourceStore.findById(did);
if (existingResource == null) {
return ServiceResult.notFound(notFoundMessage(did));
}
var publisher = registry.getPublisher(did);
if (publisher == null) {
return ServiceResult.badRequest(noPublisherFoundMessage(did));
}
// only unpublish if published, NOOP otherwise
if (existingDoc.getState() == DidState.PUBLISHED.code()) {
var publishResult = publisher.unpublish(did);
return publishResult.succeeded() ?
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());
}
monitor.info("Unpublishing DID Document '%s': not in state '%s', unpublishing is a NOOP.".formatted(did, existingDoc.getStateAsEnum()));
return success();

var participantId = existingResource.getParticipantId();
return participantContextService.getParticipantContext(participantId)
.map(ParticipantContext::getStateAsEnum)
.compose(state -> {
var canUnpublish = state.equals(ParticipantContextState.DEACTIVATED);
if (canUnpublish) {
var publisher = registry.getPublisher(did);
if (publisher == null) {
return ServiceResult.badRequest(noPublisherFoundMessage(did));
}
// only unpublish if published, NOOP otherwise
if (existingResource.getState() == DidState.PUBLISHED.code()) {
var publishResult = publisher.unpublish(did);
return publishResult.succeeded() ?
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());
}
monitor.info("Unpublishing DID Document '%s': not in state '%s', unpublishing is a NOOP.".formatted(did, existingResource.getStateAsEnum()));
return success();
}
return ServiceResult.badRequest(("Cannot un-publish DID '%s' for participant '%s' because the ParticipantContext is not state '%s' state, " +
"but was '%s'.")
.formatted(did, participantId, ParticipantContextState.DEACTIVATED, state));
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.edc.identithub.spi.did.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
Expand Down Expand Up @@ -46,6 +47,8 @@ public class DidServicesExtension implements ServiceExtension {

@Inject
private KeyParserRegistry keyParserRegistry;
@Inject
private ParticipantContextService participantContextService;

@Override
public String name() {
Expand All @@ -62,7 +65,8 @@ public DidDocumentPublisherRegistry getDidPublisherRegistry() {

@Provider
public DidDocumentService createDidDocumentService(ServiceExtensionContext context) {
var service = new DidDocumentServiceImpl(transactionContext, didResourceStore, getDidPublisherRegistry(), context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry);
var service = new DidDocumentServiceImpl(transactionContext, didResourceStore,
getDidPublisherRegistry(), participantContextService, context.getMonitor().withPrefix("DidDocumentService"), keyParserRegistry);
eventRouter.registerSync(ParticipantContextUpdated.class, service);
eventRouter.registerSync(KeyPairAdded.class, service);
eventRouter.registerSync(KeyPairRevoked.class, service);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import org.eclipse.edc.identithub.spi.did.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState;
import org.eclipse.edc.keys.KeyParserRegistryImpl;
import org.eclipse.edc.keys.keyparsers.JwkParser;
Expand All @@ -38,6 +40,7 @@
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.transaction.spi.NoopTransactionContext;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -60,9 +63,12 @@
import static org.mockito.Mockito.when;

class DidDocumentServiceImplTest {
public static final String TEST_DID = "did:web:testdid";
private static final String TEST_PARTICIPANT_ID = "test-participant";
private final DidResourceStore didResourceStoreMock = mock();
private final DidDocumentPublisherRegistry publisherRegistry = mock();
private final DidDocumentPublisher publisherMock = mock();
private final ParticipantContextService participantContextServiceMock = mock();
private DidDocumentServiceImpl service;
private Monitor monitorMock;

Expand All @@ -75,7 +81,13 @@ void setUp() {
registry.register(new JwkParser(new ObjectMapper(), mock()));
registry.register(new PemParser(mock()));
monitorMock = mock();
service = new DidDocumentServiceImpl(trx, didResourceStoreMock, publisherRegistry, monitorMock, registry);
service = new DidDocumentServiceImpl(trx, didResourceStoreMock, publisherRegistry, participantContextServiceMock, monitorMock, registry);

when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.ACTIVATED)
.build()));
}

@Test
Expand Down Expand Up @@ -200,6 +212,11 @@ void unpublish() {
var did = doc.getId();
when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build());
when(publisherMock.unpublish(did)).thenReturn(Result.success());
when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.DEACTIVATED)
.build()));

assertThat(service.unpublish(did)).isSucceeded();

Expand All @@ -226,6 +243,11 @@ void unpublish_noPublisherFound() {
var did = doc.getId();
when(publisherRegistry.getPublisher(any())).thenReturn(null);
when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build());
when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.DEACTIVATED)
.build()));

assertThat(service.unpublish(did)).isFailed().detail()
.isEqualTo(service.noPublisherFoundMessage(did));
Expand All @@ -241,7 +263,12 @@ void unpublish_publisherReportsError() {
var did = doc.getId();
when(didResourceStoreMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build());
when(publisherMock.unpublish(did)).thenReturn(Result.failure("test-failure"));

when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.DEACTIVATED)
.build()));

assertThat(service.unpublish(did)).isFailed()
.detail()
.isEqualTo("test-failure");
Expand Down Expand Up @@ -408,6 +435,12 @@ void onParticipantContextUpdated_whenDeactivates_shouldUnpublish() {
when(didResourceStoreMock.query(any())).thenReturn(List.of(didResource));
when(publisherMock.unpublish(anyString())).thenReturn(Result.success());

when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.DEACTIVATED)
.build()));

service.on(EventEnvelope.Builder.newInstance()
.payload(ParticipantContextUpdated.Builder.newInstance()
.newState(ParticipantContextState.DEACTIVATED)
Expand Down Expand Up @@ -454,6 +487,12 @@ void onParticipantContextUpdated_whenDeactivated_published_shouldBeNoop() {
when(didResourceStoreMock.query(any())).thenReturn(List.of(didResource));
when(publisherMock.unpublish(anyString())).thenReturn(Result.success());

when(participantContextServiceMock.getParticipantContext(any())).thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(TEST_PARTICIPANT_ID)
.apiTokenAlias("token")
.state(ParticipantContextState.DEACTIVATED)
.build()));

service.on(EventEnvelope.Builder.newInstance()
.payload(ParticipantContextUpdated.Builder.newInstance()
.newState(ParticipantContextState.DEACTIVATED)
Expand Down Expand Up @@ -572,10 +611,10 @@ public String name() {

private DidDocument.Builder createDidDocument() {
return DidDocument.Builder.newInstance()
.id("did:web:testdid")
.id(TEST_DID)
.service(List.of(new Service("test-service", "test-service", "https://test.service.com/")))
.verificationMethod(List.of(VerificationMethod.Builder.newInstance()
.id("did:web:testdid#key-1")
.id(TEST_DID + "#key-1")
.publicKeyMultibase("saflasjdflaskjdflasdkfj")
.build()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2024 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package org.eclipse.edc.identityhub.participantcontext;

import org.eclipse.edc.identithub.spi.did.DidDocumentService;
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextCreated;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleting;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.event.EventRouter;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import java.time.Clock;

import static org.eclipse.edc.identityhub.participantcontext.ParticipantContextExtension.NAME;

@Extension(NAME)
public class ParticipantContextCoordinatorExtension implements ServiceExtension {
public static final String NAME = "ParticipantContext Coordinator Extension";

@Inject
private DidDocumentService didDocumentService;
@Inject
private KeyPairService keyPairService;
@Inject
private Clock clock;
@Inject
private EventRouter eventRouter;

@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {
var coordinator = new ParticipantContextEventCoordinator(context.getMonitor().withPrefix("ParticipantContextEventCoordinator"),
didDocumentService, keyPairService);

eventRouter.registerSync(ParticipantContextCreated.class, coordinator);
eventRouter.registerSync(ParticipantContextDeleting.class, coordinator);
}
}
Loading

0 comments on commit f906ff9

Please sign in to comment.