PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

PostgreSQL-Anonymisierung bei Bedarf

Bevor, während und nachdem die DSGVO 2018 in die Stadt kam, gab es viele Ideen, um das Problem des Löschens oder Verbergens von Benutzerdaten zu lösen, indem verschiedene Ebenen des Software-Stacks, aber auch verschiedene Ansätze verwendet wurden (Hard Deletion, Soft Deletion, Anonymisierung). Anonymisierung war eine davon, die bei PostgreSQL-basierten Organisationen/Unternehmen bekanntermaßen beliebt ist.

Im Geiste der DSGVO sehen wir mehr und mehr die Anforderung, dass Geschäftsdokumente und Berichte zwischen Unternehmen ausgetauscht werden, sodass die in diesen Berichten aufgeführten Personen anonymisiert dargestellt werden, d. h. nur ihre Funktion/Titel angezeigt wird , während ihre persönlichen Daten verborgen bleiben. Dies geschieht höchstwahrscheinlich aufgrund der Tatsache, dass die Unternehmen, die diese Berichte erhalten, diese Daten nicht gemäß den Verfahren/Prozessen der DSGVO verwalten möchten, sie wollen sich nicht mit der Last befassen, neue Verfahren/Prozesse/Systeme zu entwickeln, um sie zu handhaben , und sie bitten nur darum, die bereits voranonymisierten Daten zu erhalten. Diese Anonymisierung gilt also nicht nur für diejenigen Personen, die ihren Wunsch geäußert haben, vergessen zu werden, sondern tatsächlich für alle Personen, die im Bericht erwähnt werden, was sich deutlich von den üblichen DSGVO-Praktiken unterscheidet.

In diesem Artikel werden wir uns mit der Anonymisierung beschäftigen, um dieses Problem zu lösen. Wir beginnen mit der Präsentation einer dauerhaften Lösung, d. h. einer Lösung, bei der eine Person, die das Vergessen beantragen möchte, bei allen zukünftigen Anfragen im System ausgeblendet werden soll. Darauf aufbauend werden wir dann einen Weg vorstellen, um „on demand“, d. h. kurzlebige Anonymisierung zu erreichen, was bedeutet, dass ein Anonymisierungsmechanismus implementiert wird, der gerade lange genug aktiv sein soll, bis die erforderlichen Berichte im System generiert werden. In der Lösung, die ich vorstelle, wird sich dies global auswirken, daher verwendet diese Lösung einen gierigen Ansatz, der alle Anwendungen abdeckt, mit minimalem (wenn überhaupt) Code-Umschreiben (und kommt von der Tendenz von PostgreSQL-DBAs, solche Probleme zentral zu lösen und die App zu verlassen Entwickler beschäftigen sich mit ihrer wahren Arbeitsbelastung). Die hier vorgestellten Methoden können jedoch leicht angepasst werden, um sie in begrenzten/engeren Bereichen anzuwenden.

Permanente Anonymisierung

Hier stellen wir eine Möglichkeit vor, eine Anonymisierung zu erreichen. Betrachten wir die folgende Tabelle mit Datensätzen der Mitarbeiter eines Unternehmens:

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=#

Diese Tabelle ist öffentlich, jeder kann sie abfragen und gehört zum öffentlichen Schema. Jetzt erstellen wir den grundlegenden Mechanismus für die Anonymisierung, der aus Folgendem besteht:

  • ein neues Schema zum Speichern verwandter Tabellen und Ansichten, nennen wir es anonym
  • eine Tabelle mit IDs von Personen, die vergessen werden möchten:anonym.person_anonym
  • eine Ansicht, die die anonymisierte Version von public.person bereitstellt:anonym.person
  • Einstellung des Suchpfads, um die neue Ansicht zu verwenden
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
;

Setzen wir den Suchpfad auf unsere Anwendung:

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

Warnung :Es ist wichtig, dass der Suchpfad in der Datenquellendefinition in der Anwendung korrekt eingerichtet ist. Der Leser wird ermutigt, fortgeschrittenere Möglichkeiten zur Handhabung des Suchpfads zu erkunden, z. mit einer Verwendung einer Funktion, die eine komplexere und dynamischere Logik handhaben kann. Beispielsweise könnten Sie eine Gruppe von Benutzern für die Dateneingabe (oder Rollen) angeben und sie die Tabelle public.person während des gesamten Anonymisierungsintervalls verwenden lassen (so dass sie weiterhin normale Daten sehen), während Sie eine Gruppe von Benutzern für die Verwaltung/Berichterstellung definieren (oder Rolle), für die die Anonymisierungslogik gilt.

Jetzt fragen wir unsere Personenbeziehung ab:

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=#

Nehmen wir nun an, dass Herr Singh das Unternehmen verlässt und ausdrücklich schriftlich sein Recht auf Vergessenwerden bekundet. Die Anwendung tut dies, indem sie seine ID in den Satz der „zu vergessenden“ IDs einfügt:

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

Lassen Sie uns jetzt genau die Abfrage wiederholen, die wir zuvor ausgeführt haben:

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=#

Wir können sehen, dass die Daten von Herrn Singh nicht über die Anwendung zugänglich sind.

Temporäre globale Anonymisierung

Die Hauptidee

  • Der Benutzer markiert den Beginn des Anonymisierungsintervalls (ein kurzer Zeitraum).
  • Während dieses Intervalls sind nur Auswahlen für die Tabelle namens Person erlaubt.
  • Alle Zugriffe (Auswahlen) werden für alle Datensätze in der Personentabelle anonymisiert, unabhängig von einer vorherigen Anonymisierungseinstellung.
  • Der Benutzer markiert das Ende des Anonymisierungsintervalls.

Bausteine

  • Zwei-Phasen-Commit (auch bekannt als vorbereitete Transaktionen).
  • Explizites Sperren von Tabellen.
  • Das Anonymisierungs-Setup, das wir oben im Abschnitt „Permanente Anonymisierung“ vorgenommen haben.

Implementierung

Eine spezielle Verwaltungsanwendung (z. B. namens:markStartOfAnynimizationPeriod) führt 

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

Das obige bewirkt, dass die Tabelle im SHARE-Modus gesperrt wird, sodass INSERTS, UPDATES, DELETES blockiert werden. Auch durch Starten einer zweiphasigen Commit-Transaktion (AKA vorbereitete Transaktion, in anderen Kontexten bekannt als verteilte Transaktionen oder eXtended Architecture-Transaktionen XA) befreien wir die Transaktion von der Verbindung der Sitzung, die den Beginn des Anonymisierungszeitraums markiert, während andere nachfolgende Sitzungen bestehen bleiben sich seiner Existenz bewusst. Die vorbereitete Transaktion ist eine persistente Transaktion, die nach dem Trennen der Verbindung/Sitzung, die sie gestartet hat, am Leben bleibt (über PREPARE TRANSACTION). Beachten Sie, dass die Anweisung „PREPARE TRANSACTION“ die Transaktion von der aktuellen Sitzung trennt. Die vorbereitete Transaktion kann von einer nachfolgenden Sitzung aufgenommen und entweder rückgängig gemacht oder festgeschrieben werden. Die Verwendung dieser Art von XA-Transaktionen ermöglicht es einem System, zuverlässig mit vielen verschiedenen XA-Datenquellen umzugehen und Transaktionslogik über diese (möglicherweise heterogenen) Datenquellen hinweg auszuführen. Die Gründe, warum wir es in diesem speziellen Fall verwenden:

  • um es der ausstellenden Client-Sitzung zu ermöglichen, die Sitzung zu beenden und ihre Verbindung zu trennen/freizugeben (eine Verbindung zu verlassen oder noch schlimmer „beizubehalten“ ist eine wirklich schlechte Idee, eine Verbindung sollte freigegeben werden, sobald sie funktioniert die Abfragen, die es ausführen muss)
  • um nachfolgende Sitzungen/Verbindungen in die Lage zu versetzen, die Existenz dieser vorbereiteten Transaktion abzufragen
  • um die Endsitzung in die Lage zu versetzen, diese vorbereitete Transaktion auszuführen (durch Verwendung ihres Namens), wodurch markiert wird:
    • die Freigabe der SHARE MODE-Sperre
    • das Ende des Anonymisierungszeitraums

Um zu überprüfen, ob die Transaktion aktiv ist und mit der SHARE-Sperre auf unserer Personentabelle verknüpft ist, tun wir Folgendes:

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=#

Die obige Abfrage stellt sicher, dass die benannte vorbereitete Transaktions-Personsperre aktiv ist und dass sich die zugehörige Sperre für die Tabelle person, die von dieser virtuellen Transaktion gehalten wird, tatsächlich im beabsichtigten Modus befindet:SHARE.

So, jetzt können wir die Ansicht anpassen:

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

Nun mit der neuen Definition, wenn der Benutzer eine vorbereitete Transaktion gestartet hat, wird die folgende Auswahl zurückgegeben:

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

testdb=#

was globale bedingungslose Anonymisierung bedeutet.

Jede App, die versucht, Daten von der Tabellenperson zu verwenden, erhält anonymisierte „****“ anstelle von echten Daten. Nehmen wir nun an, der Administrator dieser App entscheidet, dass der Anonymisierungszeitraum enden soll, sodass seine App jetzt Folgendes ausgibt:

COMMIT PREPARED 'personlock';

Jetzt gibt jede nachfolgende Auswahl zurück:

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=#

Warnung! :Die Sperre verhindert gleichzeitige Schreibvorgänge, verhindert jedoch nicht eventuelle Schreibvorgänge, wenn die Sperre aufgehoben wird. Es besteht also eine potenzielle Gefahr für das Aktualisieren von Apps, das Lesen von „****“ aus der Datenbank, ein unvorsichtiger Benutzer, der auf „Update“ klickt, und dann wird nach einiger Wartezeit die SHARED-Sperre freigegeben und das Update schreibt erfolgreich „*** *' anstelle dessen, wo korrekte normale Daten stehen sollten. Benutzer können hier natürlich helfen, indem sie nicht blind auf Tasten drücken, aber einige zusätzliche Schutzmaßnahmen könnten hier hinzugefügt werden. Beim Aktualisieren von Apps könnte Folgendes ausgegeben werden:

set lock_timeout TO 1;

zu Beginn der Aktualisierungstransaktion. Auf diese Weise wird jedes Warten/Blockieren von mehr als 1 ms eine Ausnahme auslösen. Das sollte vor den allermeisten Fällen schützen. Eine andere Möglichkeit wäre eine Check-Einschränkung in einem der sensiblen Felder, um den Wert „****“ zu prüfen.

ALARM! :Es ist zwingend erforderlich, dass die vorbereitete Transaktion schließlich abgeschlossen wird. Entweder von dem Benutzer, der es gestartet hat (oder einem anderen Benutzer), oder sogar von einem Cron-Skript, das alle sagen wir 30 Minuten nach vergessenen Transaktionen sucht. Das Vergessen, diese Transaktion zu beenden, führt zu katastrophalen Ergebnissen, da es die Ausführung von VACUUM verhindert, und natürlich bleibt die Sperre bestehen, wodurch Schreibvorgänge in die Datenbank verhindert werden. Wenn Sie mit Ihrem System nicht vertraut genug sind, wenn Sie nicht alle Aspekte und Nebenwirkungen der Verwendung einer vorbereiteten/verteilten Transaktion mit einer Sperre vollständig verstehen, wenn Sie keine angemessene Überwachung eingerichtet haben, insbesondere in Bezug auf MVCC Metriken, dann folgen Sie diesem Ansatz einfach nicht. In diesem Fall könnten Sie eine spezielle Tabelle mit Parametern für Verwaltungszwecke haben, in der Sie zwei spezielle Spaltenwerte verwenden könnten, einen für den normalen Betrieb und einen für die globale erzwungene Anonymisierung, oder Sie könnten mit freigegebenen Advisory-Sperren auf PostgreSQL-Anwendungsebene experimentieren:

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