Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use events to decouple services #245

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ maven/mavencentral/org.apache.velocity/velocity-engine-core/2.3, Apache-2.0, app
maven/mavencentral/org.apache.velocity/velocity-engine-scripting/2.3, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.assertj/assertj-core/3.25.1, Apache-2.0, approved, #12585
maven/mavencentral/org.assertj/assertj-core/3.25.2, Apache-2.0, approved, #12585
maven/mavencentral/org.awaitility/awaitility/4.2.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.72, MIT, approved, #3789
maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.77, MIT, approved, #11593
Expand Down
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
Loading