nuts-foundation/nuts-consent-logic

View on GitHub
pkg/logic.go

Summary

Maintainability
D
2 days
Test Coverage
/*
 *  Nuts consent logic holds the logic for consent creation
 *  Copyright (C) 2019 Nuts community
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package pkg

import (
    "context"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "sync"
    "time"

    "github.com/nuts-foundation/nuts-crypto/pkg/cert"
    core "github.com/nuts-foundation/nuts-go-core"
    "github.com/thedevsaddam/gojsonq/v2"

    bridgeClient "github.com/nuts-foundation/consent-bridge-go-client/api"
    cStore "github.com/nuts-foundation/nuts-consent-store/pkg"
    crypto "github.com/nuts-foundation/nuts-crypto/pkg"
    cryptoTypes "github.com/nuts-foundation/nuts-crypto/pkg/types"
    events "github.com/nuts-foundation/nuts-event-octopus/pkg"
    pkg2 "github.com/nuts-foundation/nuts-fhir-validation/pkg"
    "github.com/nuts-foundation/nuts-registry/pkg"
    uuid "github.com/satori/go.uuid"
    "github.com/sirupsen/logrus"
)

type ConsentLogicConfig struct {
}

type ConsentLogicClient interface {
    StartConsentFlow(*CreateConsentRequest) (*uuid.UUID, error)
    HandleIncomingCordaEvent(*events.Event)
}

type ConsentLogic struct {
    NutsRegistry     pkg.RegistryClient
    NutsCrypto       crypto.Client
    NutsConsentStore cStore.ConsentStoreClient
    NutsEventOctopus events.EventOctopusClient
    Config           ConsentLogicConfig
    EventPublisher   events.IEventPublisher
}

var instance *ConsentLogic
var oneEngine sync.Once

func logger() *logrus.Entry {
    return logrus.StandardLogger().WithField("module", "consent-logic")
}

func ConsentLogicInstance() *ConsentLogic {
    oneEngine.Do(func() {
        instance = NewConsentLogicInstance(ConsentLogicConfig{}, crypto.CryptoInstance(), pkg.RegistryInstance(), cStore.ConsentStoreInstance(), events.EventOctopusInstance())
    })
    return instance
}

func NewConsentLogicInstance(config ConsentLogicConfig, cryptoClient crypto.Client, registryClient pkg.RegistryClient, consentStoreClient cStore.ConsentStoreClient, eventOctopusClient events.EventOctopusClient) *ConsentLogic {
    return &ConsentLogic{
        Config:           config,
        NutsCrypto:       cryptoClient,
        NutsRegistry:     registryClient,
        NutsConsentStore: consentStoreClient,
        NutsEventOctopus: eventOctopusClient,
    }
}

// StartConsentFlow is the start of the consentFlow. It is a a blocking method which will fire the first event.
func (cl ConsentLogic) StartConsentFlow(createConsentRequest *CreateConsentRequest) (*uuid.UUID, error) {
    // Create the event to start the consent flow
    event, err := cl.buildConsentRequestConstructedEvent(createConsentRequest)
    if err != nil {
        return nil, err
    }

    // publish the event
    err = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
    if err != nil {
        return nil, err
    }
    // extract the events UUID
    eventUUID, err := uuid.FromString(event.UUID)
    if err != nil {
        return nil, err
    }

    logger().Debugf("Published NewConsentRequest to bridge with event: %+v", event)
    return &eventUUID, nil
}

func (cl ConsentLogic) buildConsentRequestConstructedEvent(createConsentRequest *CreateConsentRequest) (*events.Event, error) {
    var err error
    var consentID string
    var records []bridgeClient.ConsentRecord
    legalEntities := []bridgeClient.Identifier{
        bridgeClient.Identifier(createConsentRequest.Actor.String()),
        bridgeClient.Identifier(createConsentRequest.Custodian.String()),
    }

    {
        if !cl.NutsCrypto.PrivateKeyExists(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: createConsentRequest.Custodian.String()})) {
            return nil, errors.New("custodian is not a known organization (no private key found on this node)")
        }
        logger().Debug("Custodian is known")
    }
    {
        if consentID, err = cl.getConsentID(*createConsentRequest); consentID == "" || err != nil {
            fmt.Println(err)
            // todo: report back the reason why the consentID could not be generated. Probably because the custodian is not managed by this node?
            return nil, errors.New("could not create the consentID for this combination of subject, actor and custodian")
        }
        logger().Debug("ConsentId generated")
    }
    {

    }

    for _, record := range createConsentRequest.Records {
        var fhirConsent string
        var encryptedConsent cryptoTypes.DoubleEncryptedCipherText
        {
            if fhirConsent, err = cl.createFhirConsentResource(createConsentRequest.Custodian, createConsentRequest.Actor, createConsentRequest.Subject, createConsentRequest.Performer, record); fhirConsent == "" || err != nil {
                return nil, fmt.Errorf("could not create the FHIR consent resource: %w", err)
            }
            logger().Debug("FHIR resource created", fhirConsent)
        }
        {
            if validationResult, err := cl.validateFhirConsentResource(fhirConsent); !validationResult || err != nil {
                return nil, fmt.Errorf("the generated FHIR consent resource is invalid: %w", err)
            }
            logger().Debug("FHIR resource is valid")
        }
        {
            if encryptedConsent, err = cl.encryptFhirConsent(fhirConsent, *createConsentRequest); err != nil {
                return nil, fmt.Errorf("could not encrypt consent resource for all involved parties: %w", err)
            }
            logger().Debug("FHIR resource encrypted")
        }

        cipherText := base64.StdEncoding.EncodeToString(encryptedConsent.CipherText)
        consentRecordHash := hashFHIRConsent(fhirConsent)

        bridgeMeta := bridgeClient.Metadata{
            Domain: []bridgeClient.Domain{"medical"},
            Period: bridgeClient.Period{
                ValidFrom: record.Period.Start,
                ValidTo:   record.Period.End,
            },
            SecureKey: bridgeClient.SymmetricKey{
                Alg: "AES_GCM", //todo: fix hardcoded alg
                Iv:  base64.StdEncoding.EncodeToString(encryptedConsent.Nonce),
            },
            PreviousAttachmentHash: record.PreviousRecordhash,
            ConsentRecordHash:      consentRecordHash,
        }

        alg := "RSA-OAEP"
        for i := range encryptedConsent.CipherTextKeys {
            ctBase64 := base64.StdEncoding.EncodeToString(encryptedConsent.CipherTextKeys[i])
            bridgeMeta.OrganisationSecureKeys = append(bridgeMeta.OrganisationSecureKeys, bridgeClient.ASymmetricKey{
                Alg:         &alg,
                CipherText:  &ctBase64,
                LegalEntity: legalEntities[i],
            })
        }

        records = append(records, bridgeClient.ConsentRecord{Metadata: &bridgeMeta, CipherText: &cipherText})
    }

    // The eventID is used to follow all the events. The created corda state-branch also gets this id.
    eventID := uuid.NewV4().String()

    now := time.Now()
    nodeIdentity := core.NutsConfig().VendorID().String()
    payloadData := bridgeClient.FullConsentRequestState{
        Comment:               nil,
        ConsentId:             bridgeClient.ConsentId{ExternalId: &consentID, UUID: eventID},
        ConsentRecords:        records,
        CreatedAt:             &now,
        InitiatingLegalEntity: bridgeClient.Identifier(createConsentRequest.Custodian.String()),
        InitiatingNode:        &nodeIdentity,
        LegalEntities:         legalEntities,
        UpdatedAt:             &now,
    }

    sjs, err := json.Marshal(payloadData)
    if err != nil {
        return nil, fmt.Errorf("failed to marshall NewConsentRequest to json: %v", err)
    }
    bsjs := base64.StdEncoding.EncodeToString(sjs)

    event := &events.Event{
        UUID:                 eventID,
        Name:                 events.EventConsentRequestConstructed,
        InitiatorLegalEntity: createConsentRequest.Custodian.String(),
        RetryCount:           0,
        ExternalID:           consentID,
        Payload:              bsjs,
    }
    return event, nil
}

// HandleIncomingCordaEvent auto-acks ConsentRequests with the missing signatures
// * Get the consentRequestState by id from the consentBridge
// * For each legalEntity get its public key
func (cl ConsentLogic) HandleIncomingCordaEvent(event *events.Event) {
    logger().Infof("received event %v", event)

    crs := bridgeClient.FullConsentRequestState{}
    decodedPayload, err := base64.StdEncoding.DecodeString(event.Payload)
    if err != nil {
        errorDescription := fmt.Sprintf("%s: could not base64 decode event payload", identity())
        event.Error = &errorDescription
        event.Name = events.EventErrored
        logger().WithError(err).Error(errorDescription)
        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
    }
    if err := json.Unmarshal(decodedPayload, &crs); err != nil {
        // have event-octopus handle redelivery or cancellation
        errorDescription := fmt.Sprintf("%s: could not unmarshall event payload", identity())
        event.Error = &errorDescription
        event.Name = events.EventErrored
        logger().WithError(err).Error(errorDescription)
        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
        return
    }

    // check if all parties signed all attachments, than this request can be finalized by the initiator
    allSigned := true
    for _, cr := range crs.ConsentRecords {
        if cr.Signatures == nil || len(*cr.Signatures) != len(crs.LegalEntities) {
            allSigned = false
        }
    }

    if allSigned {
        logger().Debugf("All signatures present for UUID: %s", event.ConsentID)
        // Is this node the initiator? InitiatorLegalEntity is only set at the initiating node.
        if event.InitiatorLegalEntity != "" {

            // Now check the public keys used by the signatures
            for _, cr := range crs.ConsentRecords {
                for _, signature := range *cr.Signatures {
                    // Get the published public key from register
                    legalEntityID := signature.LegalEntity.PartyID()
                    legalEntity, err := cl.NutsRegistry.OrganizationById(legalEntityID)
                    if err != nil {
                        errorMsg := fmt.Sprintf("Could not get organization public key for: %s, err: %v", legalEntityID, err)
                        event.Error = &errorMsg
                        logger().Debug(errorMsg)
                        _ = cl.EventPublisher.Publish(events.ChannelConsentRetry, *event)
                        return
                    }

                    jwkFromSig, err := cert.MapToJwk(signature.Signature.PublicKey)
                    if err != nil {
                        errorMsg := fmt.Sprintf("%s: unable to parse signature public key as JWK: %v", identity(), err)
                        logger().Warn(errorMsg)
                        logger().Debugf("publicKey from signature: %s ", signature.Signature.PublicKey)
                        event.Name = events.EventErrored
                        event.Error = &errorMsg
                        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
                        return
                    }

                    // Check if the organization owns the public key used for signing and whether it was valid at the moment of signing.
                    // ========================
                    // TODO: Checking it against the current time is wrong; it should be the time of signing.
                    // In practice this won't cause problems for now since certificates used for signing consent records
                    // are valid for 1 year since they were introduced (april 2020). So we just have to make sure we
                    // switch to a signature format (JWS) which does contain the time of signing before april 2021.
                    // https://github.com/nuts-foundation/nuts-consent-logic/issues/45
                    checkTime := time.Now()
                    orgHasKey, err := legalEntity.HasKey(jwkFromSig, checkTime)
                    // Fixme: this error handling should be rewritten
                    if err != nil {
                        errorMsg := fmt.Sprintf("%s: could not check JWK against organization keys: %v", identity(), err)
                        logger().Warn(errorMsg)
                        event.Name = events.EventErrored
                        event.Error = &errorMsg
                        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
                        return
                    }

                    if !orgHasKey {
                        errorMsg := fmt.Sprintf("%s:  organization %s did not have a valid signature for the corresponding public key at the given time %s", core.NutsConfig().Identity(), legalEntityID, checkTime.String())
                        logger().Warn(errorMsg)
                        event.Name = events.EventErrored
                        event.Error = &errorMsg
                        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
                        return
                    }

                    // checking the actual signature here is not required since it's already checked by the CordApp.
                }
            }

            logger().Debugf("Sending FinalizeRequest to bridge for UUID: %s", event.ConsentID)
            event.Name = events.EventAllSignaturesPresent
            _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
        } else {
            logger().Debug("This node is not the initiator. Lets wait for the initiator to broadcast EventAllSignaturesPresent")
        }
        return
    }

    logger().Debugf("Handling ConsentRequestState: %+v", crs)

    for _, cr := range crs.ConsentRecords {
        // find out which legal entity is ours and still needs signing? It can be more than one, but always take first one missing.
        legalEntityToSignFor := cl.findFirstEntityToSignFor(cr.Signatures, crs.LegalEntities)

        // is there work for us?
        if legalEntityToSignFor == "" {
            // nothing to sign for this node/record.
            continue
        }

        // decrypt
        // =======
        fhirConsent, err := cl.decryptConsentRecord(cr, legalEntityToSignFor)
        if err != nil {
            errorDescription := fmt.Sprintf("%s: could not decrypt consent record", identity())
            event.Name = events.EventErrored
            event.Error = &errorDescription
            logger().WithError(err).Error(errorDescription)
            _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
            return
        }

        // validate consent record
        // =======================
        if validationResult, err := cl.validateFhirConsentResource(fhirConsent); !validationResult || err != nil {
            errorDescription := fmt.Sprintf("%s: consent record invalid", identity())
            event.Name = events.EventErrored
            event.Error = &errorDescription
            logger().WithError(err).Error(errorDescription)
            _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
            return
        }

        // publish EventConsentRequestValid
        // ===========================
        event.Name = events.EventConsentRequestValid
        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
    }
}

// HandleEventConsentRequestAcked handles the Event Consent Request Acked event. It passes a copy of the event to the
// signing step and if everything is ok, it publishes this new event to ChannelConsentRequest.
// In case of an error, it publishes the event to ChannelConsentErrored.
func (cl ConsentLogic) HandleEventConsentRequestAcked(event *events.Event) {
    var newEvent *events.Event
    var err error

    if newEvent, err = cl.signConsentRequest(*event); err != nil {
        errorMsg := fmt.Sprintf("%s: could not sign request %v", identity(), err)
        event.Name = events.EventErrored
        event.Error = &errorMsg
        _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
    }
    newEvent.Name = events.EventAttachmentSigned
    _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *newEvent)
}

func (cl ConsentLogic) signConsentRequest(event events.Event) (*events.Event, error) {
    crs := bridgeClient.FullConsentRequestState{}
    decodedPayload, err := base64.StdEncoding.DecodeString(event.Payload)
    if err != nil {
        errorDescription := "Could not base64 decode event payload"
        event.Error = &errorDescription
        logger().WithError(err).Error(errorDescription)
        return &event, nil
    }
    if err := json.Unmarshal(decodedPayload, &crs); err != nil {
        // have event-octopus handle redelivery or cancellation
        errorDescription := "Could not unmarshall event payload"
        event.Error = &errorDescription
        logger().WithError(err).Error(errorDescription)
        return &event, nil
    }

    for _, cr := range crs.ConsentRecords {
        legalEntityToSignFor := cl.findFirstEntityToSignFor(cr.Signatures, crs.LegalEntities)

        // is there work for the given record, otherwise continue till a missing signature is detected
        if legalEntityToSignFor == "" {
            // nothing to sign for this node/record.
            continue
        }

        consentRecordHash := *cr.AttachmentHash
        logger().Debugf("signing for LegalEntity %s and consentRecordHash %s", legalEntityToSignFor, consentRecordHash)

        pubKey, err := cl.NutsCrypto.GetPublicKeyAsJWK(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: legalEntityToSignFor}))
        if err != nil {
            logger().Errorf("Error in getting pubKey for %s: %v", legalEntityToSignFor, err)
            return nil, err
        }

        jwk, err := cert.JwkToMap(pubKey)
        if err != nil {
            logger().Errorf("Error in transforming pubKey for %s: %v", legalEntityToSignFor, err)
            return nil, err
        }
        hexConsentRecordHash, err := hex.DecodeString(consentRecordHash)
        if err != nil {
            logger().Errorf("Could not decode consentRecordHash into hex value %s: %v", consentRecordHash, err)
            return nil, err
        }
        sigBytes, err := cl.NutsCrypto.Sign(hexConsentRecordHash, cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: legalEntityToSignFor}))
        if err != nil {
            errorDescription := fmt.Sprintf("Could not sign consent record for %s, err: %v", legalEntityToSignFor, err)
            event.Error = &errorDescription
            logger().WithError(err).Error(errorDescription)
            return &event, err
        }
        encodedSignatureBytes := base64.StdEncoding.EncodeToString(sigBytes)
        partySignature := bridgeClient.PartyAttachmentSignature{
            Attachment:  consentRecordHash,
            LegalEntity: bridgeClient.Identifier(legalEntityToSignFor),
            Signature: bridgeClient.SignatureWithKey{
                Data:      encodedSignatureBytes,
                PublicKey: bridgeClient.JWK(jwk),
            },
        }

        payload, err := json.Marshal(partySignature)
        if err != nil {
            return nil, err
        }
        event.Payload = base64.StdEncoding.EncodeToString(payload)
        logger().Debugf("Consent request signed for %s", legalEntityToSignFor)

        return &event, nil
    }

    errorDescription := fmt.Sprintf("event with name %s recevied, but nothing to sign for this node", events.EventConsentRequestValid)
    event.Error = &errorDescription
    logger().WithError(err).Error(errorDescription)
    return &event, err
}

func (cl ConsentLogic) decryptConsentRecord(cr bridgeClient.ConsentRecord, legalEntity string) (string, error) {
    encodedCipherText := cr.CipherText
    cipherText, err := base64.StdEncoding.DecodeString(*encodedCipherText)
    // convert hex string of attachment to bytes
    if err != nil {
        return "", err
    }

    if cr.Metadata == nil {
        err := errors.New("missing metadata in consentRequest")
        logger().Error(err)
        return "", err
    }

    var encodedLegalEntityKey string
    for _, value := range cr.Metadata.OrganisationSecureKeys {
        if value.LegalEntity == bridgeClient.Identifier(legalEntity) {
            encodedLegalEntityKey = *value.CipherText
        }
    }

    if encodedLegalEntityKey == "" {
        return "", fmt.Errorf("no key found for legalEntity: %s", legalEntity)
    }
    legalEntityKey, _ := base64.StdEncoding.DecodeString(encodedLegalEntityKey)

    nonce, _ := base64.StdEncoding.DecodeString(cr.Metadata.SecureKey.Iv)
    dect := cryptoTypes.DoubleEncryptedCipherText{
        CipherText:     cipherText,
        CipherTextKeys: [][]byte{legalEntityKey},
        Nonce:          nonce,
    }
    consentRecord, err := cl.NutsCrypto.DecryptKeyAndCipherText(dect, cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: legalEntity}))
    if err != nil {
        logger().WithError(err).Error("Could not decrypt consent record")
        return "", err
    }

    return string(consentRecord), nil
}

// The node can manage more than one legalEntity. This method provides a deterministic way of selecting the current
// legalEntity to work with. It loops over all legalEntities, selects the ones that still needs to sign and selects
// the first one which is managed by this node.
func (cl ConsentLogic) findFirstEntityToSignFor(signatures *[]bridgeClient.PartyAttachmentSignature, identifiers []bridgeClient.Identifier) string {
    // fill map with signatures legalEntity for easy lookup
    attSignatures := make(map[string]bool)
    // signatures can be nil if no signatures have been set yet
    if signatures != nil {
        for _, att := range *signatures {
            attSignatures[string(att.LegalEntity)] = true
        }

    }

    // Find all LegalEntities managed by this node which still need a signature
    // for each legal entity...
    for _, ent := range identifiers {
        // ... check if it has already signed the request
        if !attSignatures[string(ent)] {
            // if not, check if this node has any keys
            if cl.NutsCrypto.PrivateKeyExists(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: string(ent)})) {
                // yes, so lets add it to the missingSignatures so we can sign it in the next step
                logger().Debugf("found first entity to sign for: %v", ent)
                return string(ent)
            }
        }
    }
    return ""
}

// HandleEventConsentRequestValid republishes every event as acked.
// TODO: This should be made optional so the ECD can perform checks and publish the ack or nack
func (cl ConsentLogic) HandleEventConsentRequestValid(event *events.Event) {
    event, _ = cl.autoAckConsentRequest(*event)
    _ = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
}

func (cl ConsentLogic) autoAckConsentRequest(event events.Event) (*events.Event, error) {
    newEvent := event
    newEvent.Name = events.EventConsentRequestAcked
    return &newEvent, nil
}

// intermediate struct to keep FHIR resource and hash together
type FHIRResourceWithHash struct {
    FHIRResource string
    // Hash represents the attachment hash (zip of cipherText and metadata) from the distributed event model
    Hash string
    // PreviousHash represents the previous attachment hash from the distributed event model (in the case of updates)
    PreviousHash *string
}

// HandleEventConsentDistributed handles EventConsentDistributed.
// This is the final step in the distributed consent state-machine.
// It decodes the payload, performs final tests and stores the relevant consentRules in the consent-store.
func (cl ConsentLogic) HandleEventConsentDistributed(event *events.Event) {
    crs := bridgeClient.ConsentState{}
    decodedPayload, err := base64.StdEncoding.DecodeString(event.Payload)
    if err != nil {
        logger().Errorf("Unable to base64 decode event payload")
        return
    }
    if err := json.Unmarshal(decodedPayload, &crs); err != nil {
        logger().Errorf("Unable to unmarshal event payload")
        return
    }

    var fhirConsents = map[string]FHIRResourceWithHash{}

    for _, cr := range crs.ConsentRecords {
        for _, organisation := range cr.Metadata.OrganisationSecureKeys {
            if !cl.NutsCrypto.PrivateKeyExists(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: string(organisation.LegalEntity)})) {
                // this organisation is not managed by this node, try with next
                continue
            }

            fhirConsentString, err := cl.decryptConsentRecord(cr, string(organisation.LegalEntity))
            if err != nil {
                logger().Error("Could not decrypt fhir consent")
                return
            }
            fhirConsents[*cr.AttachmentHash] = FHIRResourceWithHash{
                Hash:         *cr.AttachmentHash,
                PreviousHash: cr.Metadata.PreviousAttachmentHash,
                FHIRResource: fhirConsentString,
            }
        }
    }

    patientConsent := cl.PatientConsentFromFHIRRecord(fhirConsents)
    patientConsent.ID = *crs.ConsentId.ExternalId

    if relevant := cl.isRelevantForThisNode(patientConsent); !relevant {
        logger().Error("Got a patientConsent irrelevant for this node")
        return
    }

    logger().Debugf("received patientConsent with %d consentRecords", len(patientConsent.Records))
    logger().Debugf("Storing consent: %+v", patientConsent)

    err = cl.NutsConsentStore.RecordConsent(context.Background(), []cStore.PatientConsent{patientConsent})
    if err != nil {
        logger().WithError(err).Error("unable to record the consents")
        return
    }

    event.Name = events.EventCompleted
    err = cl.EventPublisher.Publish(events.ChannelConsentRequest, *event)
    if err != nil {
        logger().WithError(err).Error("unable to publish the EventCompleted event")
        return
    }
}

// hashFHIRConsent generates a consistent hash of the fhirConsent record
func hashFHIRConsent(fhirConsent string) string {
    return fmt.Sprintf("%x", sha256.Sum256([]byte(fhirConsent)))
}

// only consent records of which or the custodian or the actor is managed by this node should be stored
func (cl ConsentLogic) isRelevantForThisNode(patientConsent cStore.PatientConsent) bool {
    // add if custodian is managed by this node
    return cl.NutsCrypto.PrivateKeyExists(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: patientConsent.Custodian})) ||
        cl.NutsCrypto.PrivateKeyExists(cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: patientConsent.Actor}))
}

// PatientConsentFromFHIRRecord extracts the PatientConsent from a FHIR consent record encoded as json string.
func (ConsentLogic) PatientConsentFromFHIRRecord(fhirConsents map[string]FHIRResourceWithHash) cStore.PatientConsent {
    var patientConsent cStore.PatientConsent

    // FixMe: we should add a check if the actors, subjects and custodians are all the same for each of these fhirConsents
    for _, consent := range fhirConsents {
        fhirConsent := gojsonq.New().JSONString(consent.FHIRResource)
        patientConsent.Actor = string(pkg2.ActorsFrom(fhirConsent)[0])
        patientConsent.Custodian = pkg2.CustodianFrom(fhirConsent)
        patientConsent.Subject = pkg2.SubjectFrom(fhirConsent)
        dataClasses := cStore.DataClassesFromStrings(pkg2.ResourcesFrom(fhirConsent))
        period := pkg2.PeriodFrom(fhirConsent)
        patientConsent.Records = append(patientConsent.Records, cStore.ConsentRecord{DataClasses: dataClasses, ValidFrom: *period[0], ValidTo: period[1], Hash: consent.Hash, PreviousHash: consent.PreviousHash})
    }

    return patientConsent
}

func (ConsentLogic) Configure() error {
    return nil
}

// Start starts a new ConsentLogic engine. It populates the ConsentLogic struct with client from other engines and
// subscribes to nats.io event.
func (cl *ConsentLogic) Start() error {
    // This module has no mode feature (server/client) so we delegate it completely to the global mode
    if core.NutsConfig().GetEngineMode("") != core.ServerEngineMode {
        return nil
    }
    publisher, err := cl.NutsEventOctopus.EventPublisher("consent-logic")
    if err != nil {
        logger().WithError(err).Panic("Could not subscribe to event publisher")
    }
    cl.EventPublisher = publisher

    err = cl.NutsEventOctopus.Subscribe("consent-logic",
        events.ChannelConsentRequest,
        map[string]events.EventHandlerCallback{
            events.EventDistributedConsentRequestReceived: cl.HandleIncomingCordaEvent,
            events.EventConsentRequestValid:               cl.HandleEventConsentRequestValid,
            events.EventConsentRequestAcked:               cl.HandleEventConsentRequestAcked,
            events.EventConsentDistributed:                cl.HandleEventConsentDistributed,
        })
    if err != nil {
        panic(err)
    }
    return nil
}

// Shutdown is currently a placeholder method. It an be used for unsubscription or other things.
func (ConsentLogic) Shutdown() error {
    // Stub
    return nil
}

func identity() string {
    return core.NutsConfig().Identity()
}