PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Anonymisation PostgreSQL à la demande

Avant, pendant et après l'entrée en vigueur du RGPD en 2018, il y a eu de nombreuses idées pour résoudre le problème de la suppression ou du masquage des données utilisateur, en utilisant différentes couches de la pile logicielle mais aussi en utilisant diverses approches (suppression matérielle, suppression logicielle, anonymisation). L'anonymisation est l'une d'elles, connue pour être populaire parmi les organisations/entreprises basées sur PostgreSQL.

Dans l'esprit du RGPD, nous voyons de plus en plus l'exigence de documents commerciaux et de rapports échangés entre les entreprises, de sorte que les individus figurant dans ces rapports soient présentés de manière anonyme, c'est-à-dire que seul leur rôle/titre est affiché , tandis que leurs données personnelles sont cachées. Cela se produit très probablement en raison du fait que les entreprises qui reçoivent ces rapports ne veulent pas gérer ces données dans le cadre des procédures/processus du RGPD, elles ne veulent pas faire face à la charge de concevoir de nouvelles procédures/processus/systèmes pour les gérer , et ils demandent juste à recevoir les données déjà pré-anonymisées. Ainsi, cette anonymisation ne s'applique pas seulement aux personnes qui ont exprimé leur souhait d'être oubliées, mais en réalité à toutes les personnes mentionnées dans le rapport, ce qui est assez différent des pratiques courantes du RGPD.

Dans cet article, nous allons traiter de l'anonymisation vers une solution à ce problème. Nous commencerons par présenter une solution permanente, c'est-à-dire une solution dans laquelle une personne demandant à être oubliée devrait être masquée dans toutes les demandes futures du système. En plus de cela, nous présenterons un moyen d'obtenir une anonymisation "à la demande", c'est-à-dire une anonymisation de courte durée, ce qui signifie la mise en œuvre d'un mécanisme d'anonymisation destiné à être actif juste assez longtemps jusqu'à ce que les rapports nécessaires soient générés dans le système. Dans la solution que je présente, cela aura un effet global, donc cette solution utilise une approche gourmande, couvrant toutes les applications, avec une réécriture de code minimale (le cas échéant) (et vient de la tendance des DBA PostgreSQL à résoudre ces problèmes de manière centralisée en quittant l'application développeurs gèrent leur véritable charge de travail). Cependant, les méthodes présentées ici peuvent être facilement modifiées pour être appliquées dans des domaines limités/étroits.

Anonymisation permanente

Nous allons présenter ici un moyen de parvenir à l'anonymisation. Considérons le tableau suivant contenant les enregistrements des employés d'une entreprise :

testdb=# create table person(id serial primary key, surname text not null, givenname text not null, midname text, address text not null, email text not null, role text not null, rank text not null);
CREATE TABLE
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Singh','Kumar','2 some street, Mumbai, India','[email protected]','Seafarer','Captain');
INSERT 0 1
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Mantzios','Achilleas','Agiou Titou 10, Iraklio, Crete, Greece','[email protected]','IT','DBA');
INSERT 0 1
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Emanuel','Tsatsadakis','Knossou 300, Iraklio, Crete, Greece','[email protected]','IT','Developer');
INSERT 0 1
testdb=#

Cette table est publique, tout le monde peut l'interroger et appartient au schéma public. Nous créons maintenant le mécanisme de base pour l'anonymisation qui consiste à :

  • un nouveau schéma pour contenir des tables et des vues associées, appelons cela anonyme
  • un tableau contenant les identifiants des personnes qui veulent être oubliées :anonym.person_anonym
  • une vue fournissant la version anonymisée de public.person :anonym.person
  • configuration du search_path, pour utiliser la nouvelle vue
testdb=# create schema anonym;
CREATE SCHEMA
testdb=# create table anonym.person_anonym(id INT NOT NULL REFERENCES public.person(id));
CREATE TABLE
CREATE OR REPLACE VIEW anonym.person AS
SELECT p.id,
    CASE
        WHEN pa.id IS NULL THEN p.givenname
        ELSE '****'::character varying
    END AS givenname,
    CASE
        WHEN pa.id IS NULL THEN p.midname
        ELSE '****'::character varying
    END AS midname,
    CASE
        WHEN pa.id IS NULL THEN p.surname
        ELSE '****'::character varying
    END AS surname,
    CASE
        WHEN pa.id IS NULL THEN p.address
        ELSE '****'::text
    END AS address,
    CASE
        WHEN pa.id IS NULL THEN p.email
        ELSE '****'::character varying
    END AS email,
    role,
    rank
  FROM person p
LEFT JOIN anonym.person_anonym pa ON p.id = pa.id
;

Définissons le search_path vers notre application :

set search_path = anonym,"$user", public;

Avertissement :il est essentiel que le search_path soit correctement configuré dans la définition de la source de données dans l'application. Le lecteur est encouragé à explorer des moyens plus avancés de gérer le chemin de recherche, par ex. avec une utilisation d'une fonction qui peut gérer une logique plus complexe et dynamique. Par exemple, vous pouvez spécifier un ensemble d'utilisateurs de saisie de données (ou rôle) et les laisser continuer à utiliser la table public.person tout au long de l'intervalle d'anonymisation (afin qu'ils continuent à voir des données normales), tout en définissant un ensemble d'utilisateurs de gestion/rapports (ou rôle) pour qui la logique d'anonymisation s'appliquera.

Interrogeons maintenant notre relation de personne :

testdb=# select * from person;
-[ RECORD 1 ]-------------------------------------
id    | 2
givenname | Achilleas
midname   |
surname   | Mantzios
address   | Agiou Titou 10, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | DBA
-[ RECORD 2 ]-------------------------------------
id    | 1
givenname | Kumar
midname   |
surname   | Singh
address   | 2 some street, Mumbai, India
email | [email protected]
role  | Seafarer
rank  | Captain
-[ RECORD 3 ]-------------------------------------
id    | 3
givenname | Tsatsadakis
midname   |
surname   | Emanuel
address   | Knossou 300, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | Developer

testdb=#

Maintenant, supposons que M. Singh quitte l'entreprise et exprime explicitement son droit à l'oubli par une déclaration écrite. L'application le fait en insérant son identifiant dans l'ensemble des identifiants "à oublier" :

testdb=# insert into anonym.person_anonym (id) VALUES(1);
INSERT 0 1

Répétons maintenant la requête exacte que nous avons exécutée auparavant :

testdb=# select * from person;
-[ RECORD 1 ]-------------------------------------
id    | 1
givenname | ****
midname   | ****
surname   | ****
address   | ****
email | ****
role  | Seafarer
rank  | Captain
-[ RECORD 2 ]-------------------------------------
id    | 2
givenname | Achilleas
midname   |
surname   | Mantzios
address   | Agiou Titou 10, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | DBA
-[ RECORD 3 ]-------------------------------------
id    | 3
givenname | Tsatsadakis
midname   |
surname   | Emanuel
address   | Knossou 300, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | Developer

testdb=#

On constate que les coordonnées de Mr Singh ne sont pas accessibles depuis l'application.

Anonymisation globale temporaire

L'idée principale

  • L'utilisateur marque le début de l'intervalle d'anonymisation (une courte période).
  • Pendant cet intervalle, seules les sélections sont autorisées pour la table nommée person.
  • Tous les accès (sélections) sont rendus anonymes pour tous les enregistrements de la table des personnes, quelle que soit la configuration d'anonymisation préalable.
  • L'utilisateur marque la fin de l'intervalle d'anonymisation.

Blocs de construction

  • Commit en deux phases (alias Transactions préparées).
  • Verrouillage explicite des tables.
  • La configuration de l'anonymisation que nous avons effectuée ci-dessus dans la section "Anonymisation permanente".

Mise en œuvre

Une application d'administration spéciale (par exemple, appelée :markStartOfAnynimizationPeriod) effectue 

testdb=# BEGIN ;
BEGIN
testdb=# LOCK public.person IN SHARE MODE ;
LOCK TABLE
testdb=# PREPARE TRANSACTION 'personlock';
PREPARE TRANSACTION
testdb=#

Ce que fait ce qui précède est d'acquérir un verrou sur la table en mode SHARE afin que les INSERTS, UPDATES, DELETES soient bloqués. De plus, en démarrant une transaction de validation en deux phases (transaction préparée AKA, dans d'autres contextes appelées transactions distribuées ou transactions eXtended Architecture XA), nous libérons la transaction de la connexion de la session marquant le début de la période d'anonymisation, tout en laissant d'autres sessions ultérieures être conscient de son existence. La transaction préparée est une transaction persistante qui reste active après la déconnexion de la connexion/session qui l'a démarrée (via PREPARE TRANSACTION). Notez que l'instruction "PREPARE TRANSACTION" dissocie la transaction de la session en cours. La transaction préparée peut être récupérée par une session ultérieure et être annulée ou validée. L'utilisation de ce type de transactions XA permet à un système de traiter de manière fiable de nombreuses sources de données XA différentes et d'exécuter une logique transactionnelle sur ces sources de données (éventuellement hétérogènes). Cependant, les raisons pour lesquelles nous l'utilisons dans ce cas précis :

  • pour permettre à la session client émettrice de mettre fin à la session et de se déconnecter/libérer sa connexion (laisser ou pire encore « persister » une connexion est une très mauvaise idée, une connexion doit être libérée dès qu'elle s'exécute les requêtes qu'il doit faire)
  • pour rendre les sessions/connexions ultérieures capables de demander l'existence de cette transaction préparée
  • pour rendre la session de fin capable de valider cette transaction préparée (par l'utilisation de son nom) marquant ainsi :
    • la libération du verrou SHARE MODE
    • la fin de la période d'anonymisation

Afin de vérifier que la transaction est active et associée au verrou SHARE sur notre table person nous faisons :

testdb=# select px.*,l0.* from pg_prepared_xacts px , pg_locks l0 where px.gid='personlock' AND l0.virtualtransaction='-1/'||px.transaction AND l0.relation='public.person'::regclass AND l0.mode='ShareLock';
-[ RECORD 1 ]------+----------------------------
transaction    | 725
gid            | personlock
prepared       | 2020-05-23 15:34:47.2155+03
owner          | postgres
database       | testdb
locktype       | relation
database       | 16384
relation       | 32829
page           |
tuple          |
virtualxid     |
transactionid  |
classid        |
objid          |
objsubid       |
virtualtransaction | -1/725
pid            |
mode           | ShareLock
granted        | t
fastpath       | f

testdb=#

Ce que fait la requête ci-dessus est de s'assurer que la transaction préparée nommée personlock est active et que le verrou associé sur la table person détenu par cette transaction virtuelle est dans le mode prévu :SHARE.

Alors maintenant, nous pouvons modifier la vue :

CREATE OR REPLACE VIEW anonym.person AS
WITH perlockqry AS (
    SELECT 1
      FROM pg_prepared_xacts px,
        pg_locks l0
      WHERE px.gid = 'personlock'::text AND l0.virtualtransaction = ('-1/'::text || px.transaction) AND l0.relation = 'public.person'::regclass::oid AND l0.mode = 'ShareLock'::text
    )
SELECT p.id,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.givenname::character varying
        ELSE '****'::character varying
    END AS givenname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.midname::character varying
        ELSE '****'::character varying
    END AS midname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.surname::character varying
        ELSE '****'::character varying
    END AS surname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.address
        ELSE '****'::text
    END AS address,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.email::character varying
        ELSE '****'::character varying
    END AS email,
p.role,
p.rank
  FROM public.person p
LEFT JOIN person_anonym pa ON p.id = pa.id

Maintenant, avec la nouvelle définition, si l'utilisateur a démarré la transaction préparée personlock, la sélection suivante sera renvoyée :

testdb=# select * from person;
id | givenname | midname | surname | address | email |   role   |   rank   
----+-----------+---------+---------+---------+-------+----------+-----------
  1 | ****  | **** | **** | **** | ****  | Seafarer | Captain
  2 | ****  | **** | **** | **** | ****  | IT   | DBA
  3 | ****  | **** | **** | **** | ****  | IT   | Developer
(3 rows)

testdb=#

qui signifie anonymisation globale inconditionnelle.

Toute application essayant d'utiliser les données d'une personne de la table obtiendra un "****" anonymisé au lieu de données réelles. Supposons maintenant que l'administrateur de cette application décide que la période d'anonymisation doit se terminer, donc son application émet maintenant :

COMMIT PREPARED 'personlock';

Maintenant, toute sélection suivante renverra :

testdb=# select * from person;
id |  givenname  | midname | surname  |            address             |         email         |   role   |   rank   
----+-------------+---------+----------+----------------------------------------+-------------------------------+----------+-----------
  1 | ****    | **** | **** | ****                               | ****                      | Seafarer | Captain
  2 | Achilleas   |     | Mantzios | Agiou Titou 10, Iraklio, Crete, Greece | [email protected]   | IT   | DBA
  3 | Tsatsadakis |     | Emanuel  | Knossou 300, Iraklio, Crete, Greece | [email protected] | IT   | Developer
(3 rows)

testdb=#

Attention ! :Le verrou empêche les écritures concurrentes, mais n'empêche pas les écritures éventuelles lorsque le verrou aura été relâché. Il existe donc un danger potentiel pour la mise à jour des applications, la lecture de '****' dans la base de données, un utilisateur négligent, la mise à jour, puis après une certaine période d'attente, le verrou SHARED est libéré et la mise à jour réussit à écrire '*** *' à la place des données normales correctes. Les utilisateurs peuvent bien sûr aider ici en n'appuyant pas aveuglément sur les boutons, mais certaines protections supplémentaires pourraient être ajoutées ici. La mise à jour des applications peut générer :

set lock_timeout TO 1;

au début de la transaction de mise à jour. De cette façon, toute attente/blocage de plus de 1 ms déclenchera une exception. Ce qui devrait protéger contre la grande majorité des cas. Une autre façon serait une contrainte de vérification dans l'un des champs sensibles pour vérifier par rapport à la valeur "****".

ALARME ! :il est impératif que la transaction préparée soit éventuellement réalisée. Soit par l'utilisateur qui l'a lancé (ou un autre utilisateur), soit même par un script cron qui vérifie les transactions oubliées toutes les disons 30 minutes. Oublier de terminer cette transaction entraînera des résultats catastrophiques car cela empêchera VACUUM de s'exécuter, et bien sûr le verrou sera toujours là, empêchant les écritures dans la base de données. Si vous n'êtes pas suffisamment à l'aise avec votre système, si vous ne comprenez pas parfaitement tous les aspects et tous les effets secondaires de l'utilisation d'une transaction préparée/distribuée avec un verrou, si vous n'avez pas mis en place une surveillance adéquate, notamment en ce qui concerne le MVCC métriques, alors ne suivez tout simplement pas cette approche. Dans ce cas, vous pouvez avoir une table spéciale contenant des paramètres à des fins d'administration où vous pouvez utiliser deux valeurs de colonne spéciales, une pour le fonctionnement normal et une pour l'anonymisation globale appliquée, ou vous pouvez expérimenter les verrous consultatifs partagés au niveau de l'application PostgreSQL :

  • https://www.postgresql.org/docs/10/explicit-locking.html#ADVISORY-LOCKS
  • https://www.postgresql.org/docs/10/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS