Skip to content

Commit

Permalink
feat: use events to decouple services (#245)
Browse files Browse the repository at this point in the history
* events: participantcontext

* decouple participantcontext service

* KeyPairService consumes participant events

* DidDocumentService consumes PArticipantContext events

* moved participant context events to "events" package

* added KeyPairEvent classes

* KeyPairService emits events

* Added test assertions to e2e test

* added events to the did publisher

* checkstyle

* DEPENDENCIES

* javadoc

* pr remarks - jackson annotations, tests

* pr remarks - doc

* Apply suggestions from code review

Co-authored-by: Jim Marino <[email protected]>

* Update spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/events/diddocument/DidDocumentEvent.java

Co-authored-by: Jim Marino <[email protected]>

* react to KeyPairAdded and KeyPairRevoked

* pr remarks

---------

Co-authored-by: Jim Marino <[email protected]>
  • Loading branch information
paullatzelsperger and jimmarino authored Jan 30, 2024
1 parent 50db5b9 commit a2f6cdb
Show file tree
Hide file tree
Showing 50 changed files with 2,027 additions and 197 deletions.
3 changes: 2 additions & 1 deletion core/identity-hub-did/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ dependencies {
api(project(":spi:identity-hub-did-spi"))

implementation(libs.edc.core.connector) // for the reflection-based query resolver

implementation(libs.edc.common.crypto)

testImplementation(libs.edc.junit)
testImplementation(libs.edc.ext.jsonld)
testImplementation(testFixtures(project(":spi:identity-hub-spi")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,55 @@

import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
import org.eclipse.edc.identithub.did.spi.DidDocumentPublisherRegistry;
import org.eclipse.edc.identithub.did.spi.DidDocumentService;
import org.eclipse.edc.identithub.did.spi.model.DidResource;
import org.eclipse.edc.identithub.did.spi.model.DidState;
import org.eclipse.edc.identithub.did.spi.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.events.keypair.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.events.keypair.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextCreated;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextDeleted;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextUpdated;
import org.eclipse.edc.security.token.jwt.CryptoConverter;
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.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.AbstractResult;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.spi.security.KeyParserRegistry;
import org.eclipse.edc.transaction.spi.TransactionContext;

import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collection;
import java.util.stream.Collectors;

import static org.eclipse.edc.spi.result.ServiceResult.success;

/**
* This is an aggregate service to manage CRUD operations of {@link DidDocument}s as well as handle their
* publishing and un-publishing. All methods are executed transactionally.
*/
public class DidDocumentServiceImpl implements DidDocumentService {
public class DidDocumentServiceImpl implements DidDocumentService, EventSubscriber {

private final TransactionContext transactionContext;
private final DidResourceStore didResourceStore;
private final DidDocumentPublisherRegistry registry;
private final Monitor monitor;
private final KeyParserRegistry keyParserRegistry;

public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry) {
public DidDocumentServiceImpl(TransactionContext transactionContext, DidResourceStore didResourceStore, DidDocumentPublisherRegistry registry, Monitor monitor, KeyParserRegistry keyParserRegistry) {
this.transactionContext = transactionContext;
this.didResourceStore = didResourceStore;
this.registry = registry;
this.monitor = monitor;
this.keyParserRegistry = keyParserRegistry;
}

@Override
Expand All @@ -53,7 +77,7 @@ public ServiceResult<Void> store(DidDocument document, String participantId) {
.build();
var result = didResourceStore.save(res);
return result.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.fromFailure(result);
});
}
Expand All @@ -70,7 +94,7 @@ public ServiceResult<Void> deleteById(String did) {
}
var res = didResourceStore.deleteById(did);
return res.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.fromFailure(res);
});
}
Expand All @@ -88,7 +112,7 @@ public ServiceResult<Void> publish(String did) {
}
var publishResult = publisher.publish(did);
return publishResult.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());

});
Expand All @@ -107,7 +131,7 @@ public ServiceResult<Void> unpublish(String did) {
}
var publishResult = publisher.unpublish(did);
return publishResult.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.badRequest(publishResult.getFailureDetail());

});
Expand All @@ -118,7 +142,7 @@ public ServiceResult<Void> unpublish(String did) {
public ServiceResult<Collection<DidDocument>> queryDocuments(QuerySpec query) {
return transactionContext.execute(() -> {
var res = didResourceStore.query(query);
return ServiceResult.success(res.stream().map(DidResource::getDocument).toList());
return success(res.stream().map(DidResource::getDocument).toList());
});
}

Expand All @@ -141,7 +165,7 @@ public ServiceResult<Void> addService(String did, Service service) {
services.add(service);
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.fromFailure(updateResult);

});
Expand All @@ -161,9 +185,8 @@ public ServiceResult<Void> replaceService(String did, Service service) {
services.add(service);
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.fromFailure(updateResult);

});
}

Expand All @@ -181,9 +204,123 @@ public ServiceResult<Void> removeService(String did, String serviceId) {
}
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
success() :
ServiceResult.fromFailure(updateResult);

});
}

@Override
public <E extends Event> void on(EventEnvelope<E> eventEnvelope) {
var payload = eventEnvelope.getPayload();
if (payload instanceof ParticipantContextCreated event) {
created(event);
} else if (payload instanceof ParticipantContextUpdated event) {
updated(event);
} else if (payload instanceof ParticipantContextDeleted event) {
deleted(event);
} else if (payload instanceof KeyPairAdded event) {
keypairAdded(event);
} else if (payload instanceof KeyPairRevoked event) {
keypairRevoked(event);
} else {
monitor.warning("KeyPairServiceImpl Received an event with unexpected payload type: %s".formatted(payload.getClass()));
}
}

private void keypairRevoked(KeyPairRevoked event) {
var didResources = findByParticipantId(event.getParticipantId());
var keyId = event.getKeyId();

var errors = didResources.stream()
.peek(didResource -> didResource.getDocument().getVerificationMethod().removeIf(vm -> vm.getId().equals(keyId)))
.map(didResourceStore::update)
.filter(StoreResult::failed)
.map(AbstractResult::getFailureDetail)
.collect(Collectors.joining(","));

if (!errors.isEmpty()) {
monitor.warning("Updating DID documents after revoking a KeyPair failed: %s".formatted(errors));
}
}

private void keypairAdded(KeyPairAdded event) {
var didResources = findByParticipantId(event.getParticipantId());
var serialized = event.getPublicKeySerialized();
var publicKey = keyParserRegistry.parse(serialized);

if (publicKey.failed()) {
monitor.warning("Error adding KeyPair '%s' to DID Document of participant '%s': %s".formatted(event.getKeyId(), event.getParticipantId(), publicKey.getFailureDetail()));
return;
}

var jwk = CryptoConverter.createJwk(new KeyPair((PublicKey) publicKey.getContent(), null));

var errors = didResources.stream()
.peek(dd -> dd.getDocument().getVerificationMethod().add(VerificationMethod.Builder.newInstance()
.id(event.getKeyId())
.publicKeyJwk(jwk.toJSONObject())
.controller(dd.getDocument().getId())
.build()))
.map(didResourceStore::update)
.filter(StoreResult::failed)
.map(AbstractResult::getFailureDetail)
.collect(Collectors.joining(","));

if (!errors.isEmpty()) {
monitor.warning("Updating DID documents after adding a KeyPair failed: %s".formatted(errors));
}

}

private void updated(ParticipantContextUpdated event) {
var newState = event.getNewState();
var forParticipant = findByParticipantId(event.getParticipantId());
var errors = forParticipant
.stream()
.map(resource -> switch (newState) {
case ACTIVATED -> publish(resource.getDid());
case DEACTIVATED -> unpublish(resource.getDid());
default -> ServiceResult.success();
})
.filter(AbstractResult::failed)
.map(AbstractResult::getFailureDetail)
.collect(Collectors.joining(", "));

if (!errors.isEmpty()) {
monitor.warning("Updating DID documents after updating a ParticipantContext failed: %s".formatted(errors));
}
}

private void deleted(ParticipantContextDeleted event) {
var participantId = event.getParticipantId();
//unpublish and delete all DIDs associated with that participant
var errors = findByParticipantId(participantId)
.stream()
.map(didResource -> unpublish(didResource.getDid())
.compose(u -> deleteById(didResource.getDid())))
.map(AbstractResult::getFailureDetail)
.collect(Collectors.joining(", "));

if (!errors.isEmpty()) {
monitor.warning("Unpublishing/deleting DID documents after deleting a ParticipantContext failed: %s".formatted(errors));
}
}

private Collection<DidResource> findByParticipantId(String participantId) {
return didResourceStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("participantId", "=", participantId)).build());
}


private void created(ParticipantContextCreated event) {
var manifest = event.getManifest();
var doc = DidDocument.Builder.newInstance()
.id(manifest.getDid())
.service(manifest.getServiceEndpoints().stream().toList())
// updating and adding a verification method happens as a result of the KeyPairAddedEvent
.build();
store(doc, manifest.getParticipantId())
.compose(u -> manifest.isActive() ? publish(doc.getId()) : success())
.onFailure(f -> monitor.warning("Creating a DID document after creating a ParticipantContext creation failed: %s".formatted(f.getFailureDetail())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@
import org.eclipse.edc.identithub.did.spi.DidDocumentPublisherRegistry;
import org.eclipse.edc.identithub.did.spi.DidDocumentService;
import org.eclipse.edc.identithub.did.spi.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.events.keypair.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.events.keypair.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextCreated;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextDeleted;
import org.eclipse.edc.identityhub.spi.events.participant.ParticipantContextUpdated;
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.KeyParserRegistry;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.transaction.spi.TransactionContext;

import static org.eclipse.edc.identityhub.did.DidServicesExtension.NAME;
Expand All @@ -33,8 +41,14 @@ public class DidServicesExtension implements ServiceExtension {
@Inject
private DidResourceStore didResourceStore;

@Inject
private EventRouter eventRouter;

private DidDocumentPublisherRegistry didPublisherRegistry;

@Inject
private KeyParserRegistry keyParserRegistry;

@Override
public String name() {
return NAME;
Expand All @@ -49,7 +63,13 @@ public DidDocumentPublisherRegistry getDidPublisherRegistry() {
}

@Provider
public DidDocumentService createDidDocumentService() {
return new DidDocumentServiceImpl(transactionContext, didResourceStore, getDidPublisherRegistry());
public DidDocumentService createDidDocumentService(ServiceExtensionContext context) {
var service = new DidDocumentServiceImpl(transactionContext, didResourceStore, getDidPublisherRegistry(), context.getMonitor(), keyParserRegistry);
eventRouter.registerSync(ParticipantContextCreated.class, service);
eventRouter.registerSync(ParticipantContextUpdated.class, service);
eventRouter.registerSync(ParticipantContextDeleted.class, service);
eventRouter.registerSync(KeyPairAdded.class, service);
eventRouter.registerSync(KeyPairRevoked.class, service);
return service;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

package org.eclipse.edc.identityhub.did;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.edc.connector.core.security.KeyParserRegistryImpl;
import org.eclipse.edc.connector.core.security.keyparsers.JwkParser;
import org.eclipse.edc.connector.core.security.keyparsers.PemParser;
import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
Expand Down Expand Up @@ -53,7 +57,10 @@ void setUp() {
var trx = new NoopTransactionContext();
when(publisherRegistry.getPublisher(startsWith("did:web:"))).thenReturn(publisherMock);

service = new DidDocumentServiceImpl(trx, storeMock, publisherRegistry);
var registry = new KeyParserRegistryImpl();
registry.register(new JwkParser(new ObjectMapper(), mock()));
registry.register(new PemParser(mock()));
service = new DidDocumentServiceImpl(trx, storeMock, publisherRegistry, mock(), registry);
}

@Test
Expand Down
Loading

0 comments on commit a2f6cdb

Please sign in to comment.