All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.edc.identityhub.did.DidDocumentServiceImpl Maven / Gradle / Ivy

/*
 *  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
 *
 */

package org.eclipse.edc.identityhub.did;

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.spi.did.DidDocumentPublisherRegistry;
import org.eclipse.edc.identithub.spi.did.DidDocumentService;
import org.eclipse.edc.identithub.spi.did.model.DidResource;
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.KeyPairAdded;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairRevoked;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextCreated;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextUpdated;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
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.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.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, 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, Monitor monitor, KeyParserRegistry keyParserRegistry) {
        this.transactionContext = transactionContext;
        this.didResourceStore = didResourceStore;
        this.registry = registry;
        this.monitor = monitor;
        this.keyParserRegistry = keyParserRegistry;
    }

    @Override
    public ServiceResult store(DidDocument document, String participantId) {
        return transactionContext.execute(() -> {
            var res = DidResource.Builder.newInstance()
                    .document(document)
                    .did(document.getId())
                    .participantId(participantId)
                    .build();
            var result = didResourceStore.save(res);
            return result.succeeded() ?
                    success() :
                    ServiceResult.fromFailure(result);
        });
    }

    @Override
    public ServiceResult deleteById(String did) {
        return transactionContext.execute(() -> {
            var existing = didResourceStore.findById(did);
            if (existing == null) {
                return ServiceResult.notFound(notFoundMessage(did));
            }
            if (existing.getState() == DidState.PUBLISHED.code()) {
                return ServiceResult.conflict("Cannot delete DID '%s' because it is already published. Un-publish first!".formatted(did));
            }
            var res = didResourceStore.deleteById(did);
            return res.succeeded() ?
                    success() :
                    ServiceResult.fromFailure(res);
        });
    }

    @Override
    public ServiceResult publish(String did) {
        return transactionContext.execute(() -> {
            var existingDoc = didResourceStore.findById(did);
            if (existingDoc == 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());

        });
    }

    @Override
    public ServiceResult unpublish(String did) {
        return transactionContext.execute(() -> {
            var existingDoc = didResourceStore.findById(did);
            if (existingDoc == null) {
                return ServiceResult.notFound(notFoundMessage(did));
            }
            var publisher = registry.getPublisher(did);
            if (publisher == null) {
                return ServiceResult.badRequest(noPublisherFoundMessage(did));
            }
            var publishResult = publisher.unpublish(did);
            return publishResult.succeeded() ?
                    success() :
                    ServiceResult.badRequest(publishResult.getFailureDetail());

        });
    }


    @Override
    public ServiceResult> queryDocuments(QuerySpec query) {
        return transactionContext.execute(() -> {
            var res = didResourceStore.query(query);
            return success(res.stream().map(DidResource::getDocument).toList());
        });
    }

    @Override
    public DidResource findById(String did) {
        return transactionContext.execute(() -> didResourceStore.findById(did));
    }

    @Override
    public ServiceResult addService(String did, Service service) {
        return transactionContext.execute(() -> {
            var didResource = didResourceStore.findById(did);
            if (didResource == null) {
                return ServiceResult.notFound("DID '%s' not found.".formatted(did));
            }
            var services = didResource.getDocument().getService();
            if (services.stream().anyMatch(s -> s.getId().equals(service.getId()))) {
                return ServiceResult.conflict("DID '%s' already contains a service endpoint with ID '%s'.".formatted(did, service.getId()));
            }
            services.add(service);
            var updateResult = didResourceStore.update(didResource);
            return updateResult.succeeded() ?
                    success() :
                    ServiceResult.fromFailure(updateResult);

        });
    }

    @Override
    public ServiceResult replaceService(String did, Service service) {
        return transactionContext.execute(() -> {
            var didResource = didResourceStore.findById(did);
            if (didResource == null) {
                return ServiceResult.notFound("DID '%s' not found.".formatted(did));
            }
            var services = didResource.getDocument().getService();
            if (services.stream().noneMatch(s -> s.getId().equals(service.getId()))) {
                return ServiceResult.badRequest("DID '%s' does not contain a service endpoint with ID '%s'.".formatted(did, service.getId()));
            }
            services.add(service);
            var updateResult = didResourceStore.update(didResource);
            return updateResult.succeeded() ?
                    success() :
                    ServiceResult.fromFailure(updateResult);
        });
    }

    @Override
    public ServiceResult removeService(String did, String serviceId) {
        return transactionContext.execute(() -> {
            var didResource = didResourceStore.findById(did);
            if (didResource == null) {
                return ServiceResult.notFound("DID '%s' not found.".formatted(did));
            }
            var services = didResource.getDocument().getService();
            var hasRemoved = services.removeIf(s -> s.getId().equals(serviceId));
            if (!hasRemoved) {
                return ServiceResult.badRequest("DID '%s' does not contain a service endpoint with ID '%s'.".formatted(did, serviceId));
            }
            var updateResult = didResourceStore.update(didResource);
            return updateResult.succeeded() ?
                    success() :
                    ServiceResult.fromFailure(updateResult);

        });
    }

    @Override
    public  void on(EventEnvelope eventEnvelope) {
        var payload = eventEnvelope.getPayload();
        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("Received 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());
        if (didResources.isEmpty()) {
            monitor.warning("No DidResources were found for participant '%s'. No updated will be performed.".formatted(event.getParticipantId()));
            return;
        }
        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.getKeyPairResourceId(), 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())
                        .type(event.getType())
                        .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())))
                .filter(AbstractResult::failed)
                .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 findByParticipantId(String participantId) {
        return didResourceStore.query(ParticipantResource.queryByParticipantId(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())));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy