CQRS + ES + GDPR =

Broadway Sensitive Serializer

Matteo Galacci

Software architect & Back-end Developer

Github:  https://github.com/matiux

Slack GRUSP:  matiux

Email:  m.galacci@gmail.com

Linkedin: Matteo Galacci

  • Developer since 2000
  • Consultant since 2008  
  • Software architect
  • Back-end developer
  • Domain Driven Design & Clean Code evangelist 
  • Member of PUG Romagna admins

Contesto

Realizzazione di un microservizio che coinvolge dati utente sensibili

  • GDPR Compliant, in particolare Art. 17 - diritto all'oblio
  • Consistenza dei dati garantita nel tempo
  • CQRS + ES

Requisiti business

Requisiti tecnici

Concetti | CQRS

CQRS (Command Query Responsibility Segregation) è un pattern che mira a segregare le responsabilità per le query e per i comandi, diversificando le operazioni di lettura e scrittura su un determinato modello.

Separazione a parte, ciò che andremo a persistere sarà sempre l'ultimo stato del modello come accade nei sistemi CRUD.

Questo porta a diversi oggetti concreti, separati in modelli di scrittura e modelli di lettura.

Concetti | Event sourcing

Event Store è quindi un registro di tutti gli eventi che, se riapplicati al modello che li ha generati, nello stesso ordine di generazione, lo portano all'ultimo stato.

L'idea è che i comandi eseguiti su un modello, portano all'emissione di eventi che sono memorizzati nell'Event Store.

ES, utilizzato insieme a CQRS, "trasforma" la parte di scrittura dei modelli CQRS in una successione di eventi persistiti in un Event Store, uno specifico data store che funge da registro cronologico e immutabile degli eventi.

Concetti | EVENT sourcing

Concetti | IMMUTABILITà

Con CQRS + ES, abbiamo un "archivio" di eventi in cui tutti gli eventi sono scritti in ordine cronologico e raggruppati per ciascun modello tramite l'id.

L'Event Store è immutabile per sua natura; dopo aver scritto un evento, non può più cambiare. Se necessario, verranno emessi eventi di compensazione.

Concetti | PROIEZIONI

Se l'ES è il registro cronologico di tutte le operazioni di scrittura avvenute su uno specifico Aggregato, allora una proiezione è una rappresentazione (view) specifica dell'Aggregato.

Per un singolo Aggregato potremmo avere tante view quante sono le nostre esigenze

All'emissione di un evento, un listener può ascoltare quell'evento per proiettarne una vista. Più listeners possono ascoltare lo stesso evento per proiettare view diverse dello stesso set di dati.

Concetti | PROIEZIONI

Concetti | Art.17 Diritto all'oblio

L'interessato ha il diritto di ottenere dal titolare del trattamento la cancellazione dei dati personali che lo riguardano senza ingiustificato ritardo e il titolare del trattamento ha l'obbligo di cancellare senza ingiustificato ritardo i dati personali

CQRS+ES+GDPR | recap

  • Nel modello CQRS + ES l'Event Store è immutabile
  • Per essere conforme alla GDPR, un utente può richiedere la cancellazione dei suoi dati
  • Necessità di avere dati consistenti nel lungo periodo
  • Eliminare i dati dell'utente in un sistema CQRS+ES significherebbe eliminare eventi dall' ES o modificarli. Entrambe cose che non possiamo fare.
  • Gli eventi di compensazione non possono essere utili in questo caso in quanto tornando indietro nella cronologia potremmo sempre recuperare i dati dell'utente.

Paradosso?

LA PROPOSTA

  • Persistere fin dall'inizio, eventi in cui il payload (o solo alcune parti) è criptato da una chiave specifica per ogni Aggregato
  • Finché la chiave è presente, gli eventi possono essere decriptati e criptati
  • Quando la chiave verrà eliminata (a seguito di una richiesta dell'utente), gli eventi rimarranno nell'ES, ma il payload, originariamente criptato, rimarrà tale senza possibilità di decodifica

La storia rimane invariata, ma i dati non sono comprensibili

LA PROPOSTA | payload

Normal payload

{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
        "id": "b0fce205-d816-46ac-886f-06de19236750",
        "name": "Matteo",
        "surname": "Galacci",
        "email": "m.galacci@gmail.com"
        "occurred_at": "2022-01-08T14:22:38.065+00:00",
    }
}
{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
        "id": "b0fce205-d816-46ac-886f-06de19236750",
        "name": "Matteo",
        "surname": "#-#2Iuofg4NJ...rbmQ==:bxQo+zXfjUgrD0jHuht0mQ==",
        "email": "#-#OFLfN9XDKt6...wtam0pcqs6vDJFRU=:bxQo+zXfjUgrD0jHuht0mQ==",
        "occurred_at": "2022-01-08T14:22:38.065+00:00",
    }
}

Sensitized payload

LA PROPOSTA | proiezioni

  • L'operazione di sensibilizzazione viene eseguita in un momento diverso dalla proiezione dell'evento
  • Le viste avranno quindi i dati in chiaro per consentire il corretto funzionamento delle operazioni di lettura.

LA PROPOSTA | proiezioni

  • Cancellare la sua chiave crittografica
  • Cancellare le viste (i read models) che contengono i suoi dati
  • Proiettare nuovamente gli eventi per rigenerare le viste con dati crittografati *

Quando un utente si avvale del diritto all'oblio, dovresti fare tre cose:

* Non essendoci la chiave crittografica la lettura dall'ES produrrà idratazione con dati non in chiaro. Questo ovviamente comporta particolari verifiche nei Value Objects o nell'Aggregato

LA PROPOSTA | proiezioni

LA PROPOSTA

Questo progetto non riguarda la sicurezza generale o la fuga di dati. 

Cosa non è Broadway Sensitive Serializer

  • L'idea è quella di rendere un sistema CQRS+ES conforme al diritto all'oblio, mantenendo il sistema consistente.
  • Puoi utilizzare questa libreria anche in un contesto diverso dalla  GDPR, dal momento che in pratica questa libreria non fa altro che decorare il serializzatore di Broadway, dandogli la possibilità di criptare e decriptare il payload degli eventi.

Cosa è Broadway Sensitive Serializer

IMPLEMENTAZIONE

Un client chiederà all'aggregato User di creare un nuovo utente, che non solo creerà l'istanza, ma anche il relativo evento, UserCreated.

Aggregate and persistence

IMPLEMENTAZIONE

class BroadwayUsers extends EventSourcingRepository implements Users
{
    public function add(User $user): void
    {
        parent::save($user);
    }
}

class User extends EventSourcedAggregateRoot
{
    public static function crea(
            UserId $userId, 
            string $name, 
            string $surname, 
            Email $email, 
            DateTimeImmutable $regDate
    ): self
    {
        $user = new self();

        $user->apply(new UserCreated($userId, $name, $surname, $email, $regDate));

        return $user;
    }
}

$user = User::create($userId, $name, $surname, $email, $registrationDate);

$users->add($user);

IMPLEMENTAZIONE

Quando chiediamo a Broadway di persistere un Aggregato, l'EventSourcingRepository prende dall'Aggregato tutti gli eventi non ancora commitati e chiede all'implementazione specifica dell'Event Store di serializzarli e quindi salvarli. Ad esempio, nel caso di Broadway DBALEventStore:

Event serialization

private function insertMessage(Connection $connection, DomainMessage $domainMessage): void
{
    $data = [
        'uuid' => $this->convertIdentifierToStorageValue((string) $domainMessage->getId()),
        'playhead' => $domainMessage->getPlayhead(),
        'metadata' => json_encode(
        	$this->metadataSerializer->serialize($domainMessage->getMetadata())
        ),
        'payload' => json_encode( 
        	$this->payloadSerializer->serialize($domainMessage->getPayload()) // <<===
        ),
        'recorded_on' => $domainMessage->getRecordedOn()->toString(),
        'type' => $domainMessage->getType(),
    ];

    $connection->insert($this->tableName, $data);
}

IMPLEMENTAZIONE

  • Broadway Sensitive Serializer interviene proprio sul serializzatore.
  • Decora la serializzazione nativa di Broadway aggiungendo la possibilità di criptare e decriptare i payload degli eventi, ovvero i valori delle sue chiavi, in base a 3 strategie che vedremo più avanti
  • Quando viene creato un nuovo Aggregato, verrà generata una chiave specifica che verrà utilizzata per criptare e decriptare.

IMPLEMENTAZIONE

IMPLEMENTAZIONE

IMPLEMENTAZIONE

Persisted event payload from Broadway with DBAL driver

{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
        "id": "446effc9-4f5c-4369-8e89-91cb5c8509b9",
        "name": "Matteo",
        "surname": "Galacci",
        "email": "m.galacci@gmail.com",
        "occurred_at": "2022-01-08T14:22:38.065+00:00"
    }
}

Persisted Event Payload from Broadway with DBAL Drivers with Whole Strategy

{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
        "id": "b0fce205-d816-46ac-886f-06de19236750",
        "name": "#-#EXWLg\/JANMK\/M+DmlpnOyQ==:bxQo+zXfjUgrD0jHuht0mQ==",
        "surname": "#-#2Iuofg4NKKPLAG2kdJrbmQ==:bxQo+zXfjUgrD0jHuht0mQ==",
      	"email": "#-#OFLfN9XDKtWrmCmUb6mhY0Iz2V6wtam0pcqs6vDJFRU=:bxQo+zXfjUgrD0jHuht0mQ==",
      	"occurred_at": "2022-01-08T14:25:13.483+00:00",
    }
}

IMPLEMENTAZIONE

Persisted event payload from Broadway with DBAL driver with Partial Strategy

{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
      	"id": "96607c7a-f4cd-4dd7-a406-9cde00913f79",
        "name": "Dario",
        "surname": "#-#SXZXQsvLTCVX8Kel0yaoHg==:iEMqT4YFE7OQzKdClNaDUg==",
      	"email": "#-#jTYqDtzJ8HHabEnJMMtuaiwiFcmCkZzel5985nSf\/Ig=:iEMqT4YFE7OQzKdClNaDUg==",
      	"occurred_at": "2022-01-14T15:04:58.323+00:00"
    }
}

Persisted Event Payload from Broadway with DBAL Drivers with Custom Strategy

{
    "class": "SensitiveUser\\User\\Domain\\Event\\UserRegistered",
    "payload": {
        "id": "c9298698-b30e-40c5-8d85-624fdf57f9df",
        "name": "Matteo",
        "surname": "Galacci",
        "email": "#-#aw+tw7shnEs2px030QS9WgRmGZckEGnIeR0a8ByMkPI=:Q0jkEOZtOs56tMkc8SjP5g==",
      	"occurred_at": "2022-01-08T14:26:39.483+00:00",
    }
}

IMPLEMENTAZIONE

Double key encryption

  • Quando viene creato un nuovo Aggregato, anche la sua chiave viene creata e mantenuta in una tabella
  • Ogni aggregato ha la sua chiave in modo che possa invalidare i singoli aggregati su richiesta
  • Per migliorare la sicurezza, la chiave dell'aggregato, che chiameremo AGGREGATE_KEY, è a sua volta crittografata con ciò che chiameremo AGGREGATE_MASTER_KEY

IMPLEMENTAZIONE

AGGREGATE_KEY

  • È persistita nel database ed è in una relazione 1:1 con l'aggregato
  • È criptata con la AGGREGATE_MASTER_KEY. Questo serve , ad esempio, per evitare che gli eventi vengano decriptati a seguito di una violazione del database
  • Può essere cancellata in modo da rendere l'aggregato non più leggibile

AGGREGATE_MASTER_KEY

  • È una per tutte le AGGREGATE_KEY
  • Non è persistita nel database. È impostata in una variabile d'ambiente o in altro modo sul server. Più driver saranno disponibili in futuro per ottenere la chiave.

PRO | CONTRO

  • La MASTER_KEY
  • Difficile applicazione su un Event Store esistente

CONTRO

  • Sistema CQRS+ES conforme al diritto all'oblio, mantenendo il sistema consistente.
  • Puoi utilizzare questa libreria anche in un contesto diverso dalla  GDPR, dal momento che non fa altro che decorare il serializzatore di Broadway, dandogli la possibilità di criptare e decriptare il payload degli eventi.

PRO

ECOSISTEMA

  • broadway-sensitive-serializer
  • broadway-sensitive-serializer-dbal
  • broadway-sensitive-serializer-bundle
  • broadway-sensitive-serializer-demo

ECOSISTEMA|SYMFONY - DBAL

services:
  broadway_sensitive_serializer.aggregate_keys.dbal:
    class: Matiux\Broadway\SensitiveSerializer\Dbal\DBALAggregateKeys
    arguments:
      $connection: "@doctrine.dbal.default_connection"
      $tableName: "aggregate_keys"
      $useBinary: false
      $binaryUuidConverter: "@broadway.uuid.converter"
broadway:
    # a service definition id implementing Broadway\EventStore\EventStore
    event_store: broadway.event_store.dbal

    # a service definition id implementing Broadway\ReadModel\RepositoryFactory
    read_model: broadway.read_model.in_memory.repository_factory

    # service definition ids implementing Broadway\Serializer\Serializer
    serializer:
        #payload: broadway.simple_interface_serializer
        payload: broadway_sensitive_serializer.serializer
        readmodel: broadway.simple_interface_serializer
        metadata:  broadway.simple_interface_serializer

ECOSISTEMA|Symfony - whole

broadway_sensitive_serializer:
  #Master key to encrypt the keys of aggregates.
  #Get it from an external service or environment variable
  aggregate_master_key: 'm4$t3rS3kr3tk31'
  #For now is the only one generator implemented. 256-bit encryption key
  key_generator: open-ssl
  #To use the DBAL  implementation, install matiux/broadway-sensitive-serializer-dbal
  aggregate_keys: broadway_sensitive_serializer.aggregate_keys.dbal
  #Default implementation, of little use outside of testing
  #aggregate_keys: broadway_sensitive_serializer.aggregate_keys.in_memory
  data_manager:
    name: AES256 #For now, it is the only encryption strategy implemented
    #Encryption key to sensitize data. If null you will need to pass the key at runtime.
    #This is the convenient way
    key: null
    #Initialization vector. If null it will be generated internally and iv_encoding must
    #be set to true. This is the convenient way
    iv: null 
    #Encrypt the iv and is appends to encrypted value. It makes sense to set it to true if
    #the iv option is set to null. This is the convenient way
    iv_encoding: true
  strategy:
    name: whole
    #Enable AggregateKey model auto creation. This is the convenient way
    aggregate_key_auto_creation: true
    excluded_id_key: id # The key of the aggregate id which should not be encrypted
    excluded_keys: # List of keys to be excluded from encryption
      - occurred_at
    events: # List of events supported by the strategy
      - SensitiveUser\User\Domain\Event\AddressAdded
      - SensitiveUser\User\Domain\Event\UserRegistered

ECOSISTEMA|Symfony - Partial

broadway_sensitive_serializer:
  aggregate_master_key: 'm4$t3rS3kr3tk31'
  key_generator: open-ssl
  aggregate_keys: broadway_sensitive_serializer.aggregate_keys.dbal
  data_manager:
    name: AES256
    key: null
    iv: null 
    iv_encoding: true
  strategy:
    name: partial
    aggregate_key_auto_creation: true
    events: # List of events supported by the strategy
      - SensitiveUser\User\Domain\Event\AddressAdded:
        - address # List of keys to sensitize
      - SensitiveUser\User\Domain\Event\UserRegistered:
        - name
        - surname

ECOSISTEMA|Symfony - CUSTOM

broadway_sensitive_serializer:
  aggregate_master_key: 'm4$t3rS3kr3tk31'
  key_generator: open-ssl
  aggregate_keys: broadway_sensitive_serializer.aggregate_keys.dbal
  data_manager:
    name: AES256
    key: null
    iv: null 
    iv_encoding: true
  strategy:
    name: custom
    aggregate_key_auto_creation: true
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  SensitiveUser\User\Domain\EventSensitizer\UserRegisteredSensitizer:
    parent: Matiux\Broadway\SensitiveSerializer\Serializer\Strategy\PayloadSensitizer
    tags:
      - { name: broadway.sensitive_serializer.custom }

ECOSISTEMA|Symfony - CUSTOM

ECOSISTEMA|Symfony - CUSTOM

LINKS