sql >> Database >  >> RDS >> PostgreSQL

PostgreSQL-anonimisering op aanvraag

Voor, tijdens en nadat de AVG in 2018 in de stad kwam, waren er veel ideeën om het probleem van het verwijderen of verbergen van gebruikersgegevens op te lossen, met behulp van verschillende lagen van de softwarestack, maar ook met behulp van verschillende benaderingen (harde verwijdering, zachte verwijdering, anonimisering). Anonimisering is een van hen waarvan bekend is dat deze populair is bij op PostgreSQL gebaseerde organisaties/bedrijven.

In de geest van de AVG zien we steeds vaker de behoefte aan zakelijke documenten en rapporten die tussen bedrijven worden uitgewisseld, zodat de personen die in die rapporten worden getoond geanonimiseerd worden gepresenteerd, d.w.z. alleen hun rol/titel wordt getoond , terwijl hun persoonlijke gegevens verborgen zijn. Dit gebeurt hoogstwaarschijnlijk vanwege het feit dat de bedrijven die deze rapporten ontvangen deze gegevens niet willen beheren volgens de procedures/processen van de AVG, ze niet willen omgaan met de last van het ontwerpen van nieuwe procedures/processen/systemen om ze te verwerken , en ze vragen gewoon om de gegevens die al vooraf geanonimiseerd zijn te ontvangen. Deze anonimisering is dus niet alleen van toepassing op die personen die hebben aangegeven dat ze vergeten willen worden, maar eigenlijk op alle mensen die in het rapport worden genoemd, wat nogal verschilt van de algemene AVG-praktijken.

In dit artikel gaan we anonimisering behandelen om een ​​oplossing voor dit probleem te vinden. We zullen beginnen met het presenteren van een permanente oplossing, dat wil zeggen een oplossing waarin een persoon die vraagt ​​om vergeten te worden verborgen moet worden in alle toekomstige vragen in het systeem. Daarop voortbouwend zullen we een manier presenteren om "on demand" te bereiken, d.w.z. kortstondige anonimisering, wat inhoudt dat een anonimiseringsmechanisme wordt geïmplementeerd dat bedoeld is om net lang genoeg actief te zijn totdat de benodigde rapporten in het systeem worden gegenereerd. In de oplossing die ik presenteer, zal dit een globaal effect hebben, dus deze oplossing gebruikt een hebzuchtige benadering, die alle applicaties dekt, met minimale (indien van toepassing) herschrijving van de code (en komt voort uit de neiging van PostgreSQL DBA's om dergelijke problemen centraal op te lossen door de app te verlaten ontwikkelaars omgaan met hun echte werklast). De methoden die hier worden gepresenteerd, kunnen echter eenvoudig worden aangepast om te worden toegepast in een beperkter/smaller bereik.

Permanente anonimisering

Hier zullen we een manier presenteren om anonimisering te bereiken. Laten we eens kijken naar de volgende tabel met gegevens van de werknemers van een bedrijf:

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

Deze tabel is openbaar, iedereen kan hem opvragen en behoort tot het openbare schema. Nu creëren we het basismechanisme voor anonimisering dat bestaat uit:

  • een nieuw schema voor gerelateerde tabellen en views, laten we dit anoniem noemen
  • een tabel met id's van mensen die vergeten willen worden:anonym.person_anonym
  • een weergave met de geanonimiseerde versie van public.person:anonym.person
  • configuratie van het zoekpad, om de nieuwe weergave te gebruiken
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
;

Laten we het zoekpad naar onze applicatie instellen:

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

Waarschuwing :het is essentieel dat het zoekpad correct is ingesteld in de gegevensbrondefinitie in de toepassing. De lezer wordt aangemoedigd om meer geavanceerde manieren te verkennen om met het zoekpad om te gaan, b.v. met gebruik van een functie die complexere en dynamischere logica aankan. U kunt bijvoorbeeld een set gebruikers (of rol) voor gegevensinvoer specificeren en ze de public.person-tabel laten blijven gebruiken gedurende het anonimiseringsinterval (zodat ze normale gegevens blijven zien), terwijl u een manager-/rapportageset van gebruikers definieert (of rol) voor wie de anonimiseringslogica van toepassing is.

Laten we nu eens kijken naar onze persoonsrelatie:

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

Veronderstel nu dat de heer Singh het bedrijf verlaat en expliciet zijn recht om te worden vergeten uitdrukt door een schriftelijke verklaring. De applicatie doet dit door zijn id in te voegen in de set van "te vergeten" id's:

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

Laten we nu de exacte zoekopdracht herhalen die we eerder hebben uitgevoerd:

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

We kunnen zien dat de gegevens van de heer Singh niet toegankelijk zijn vanuit de applicatie.

Tijdelijke wereldwijde anonimisering

Het belangrijkste idee

  • De gebruiker markeert het begin van het anonimiseringsinterval (een korte periode).
  • Tijdens dit interval zijn alleen selecties toegestaan ​​voor de tafel met de naam persoon.
  • Alle toegang (selecteert) wordt geanonimiseerd voor alle records in de persoonstabel, ongeacht eventuele eerdere instellingen voor anonimisering.
  • De gebruiker markeert het einde van het anonimiseringsinterval.

Bouwstenen

  • Commit in twee fasen (ook wel Prepared Transactions genoemd).
  • Expliciete tabelvergrendeling.
  • De instelling voor anonimisering die we hierboven hebben gedaan in het gedeelte 'Permanente anonimisering'.

Implementatie

Een speciale beheerders-app (bijv. genaamd:markStartOfAnynimizationPeriod) voert 

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

Wat het bovenstaande doet, is een vergrendeling op de tafel verkrijgen in de SHARE-modus, zodat INSERTS, UPDATES, DELETES worden geblokkeerd. Ook door een tweefasige vastleggingstransactie te starten (door AKA voorbereide transactie, in andere contexten bekend als gedistribueerde transacties of eXtended Architecture-transacties XA), bevrijden we de transactie van de verbinding met de sessie die het begin van de anonimiseringsperiode markeert, terwijl we andere volgende sessies op de hoogte van het bestaan ​​ervan. De voorbereide transactie is een persistente transactie die in leven blijft na het verbreken van de verbinding/sessie waarmee deze is gestart (via PREPARE TRANSACTION). Houd er rekening mee dat de instructie "PREPARE TRANSACTION" de transactie loskoppelt van de huidige sessie. De voorbereide transactie kan worden opgehaald door een volgende sessie en ofwel worden teruggedraaid of vastgelegd. Het gebruik van dit soort XA-transacties stelt een systeem in staat om op betrouwbare wijze met veel verschillende XA-gegevensbronnen om te gaan en transactielogica uit te voeren over die (mogelijk heterogene) gegevensbronnen. De redenen waarom we het in dit specifieke geval gebruiken:

  • om de uitgevende clientsessie in staat te stellen de sessie te beëindigen en de verbinding te verbreken/beëindigen (het verlaten of, erger nog, het "aanhouden" van een verbinding is een heel slecht idee, een verbinding moet worden vrijgegeven zodra deze werkt de vragen die het moet doen)
  • om volgende sessies/verbindingen in staat te stellen het bestaan ​​van deze voorbereide transactie op te vragen
  • om de eindsessie in staat te stellen deze voorbereide transactie uit te voeren (door het gebruik van de naam) en markeert aldus:
    • de vrijgave van de SHARE MODE-vergrendeling
    • het einde van de anonimiseringsperiode

Om te verifiëren dat de transactie actief is en is gekoppeld aan de SHARE-vergrendeling op onze persoonstabel, doen we:

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

Wat de bovenstaande query doet, is ervoor zorgen dat de genoemde voorbereide transactie persoonsvergrendeling actief is en dat de gekoppelde persoon aan tafel die door deze virtuele transactie wordt vastgehouden inderdaad in de beoogde modus staat:DELEN.

Dus nu kunnen we de weergave aanpassen:

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

Nu met de nieuwe definitie, als de gebruiker is begonnen met het voorbereiden van de transactie personlock, zal de volgende selectie terugkeren:

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

testdb=#

wat betekent wereldwijde onvoorwaardelijke anonimisering.

Elke app die gegevens van een tafelpersoon probeert te gebruiken, krijgt geanonimiseerde "****" in plaats van echte echte gegevens. Laten we nu aannemen dat de beheerder van deze app besluit dat de anonimiseringsperiode afloopt, dus zijn app geeft nu problemen:

COMMIT PREPARED 'personlock';

Nu wordt elke volgende selectie geretourneerd:

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

Waarschuwing! :De vergrendeling voorkomt gelijktijdige schrijfbewerkingen, maar verhindert niet dat er uiteindelijk wordt geschreven wanneer de vergrendeling wordt opgeheven. Er is dus een potentieel gevaar voor het updaten van apps, het lezen van '****' uit de database, een onzorgvuldige gebruiker, het raken van update, en na een tijdje wachten, wordt het SHARED-slot vrijgegeven en slaagt de update erin om '***' te schrijven *' in plaats van waar correcte normale gegevens zouden moeten staan. Gebruikers kunnen hier natuurlijk helpen door niet blindelings op knoppen te drukken, maar hier zouden nog wat extra beveiligingen kunnen worden toegevoegd. Het updaten van apps kan een:

. veroorzaken
set lock_timeout TO 1;

aan het begin van de updatetransactie. Op deze manier zal elke wachttijd/blokkering langer dan 1 ms een uitzondering veroorzaken. Die zou moeten beschermen tegen de overgrote meerderheid van de gevallen. Een andere manier is een controlebeperking in een van de gevoelige velden om te controleren tegen de waarde '****'.

ALARM! :het is absoluut noodzakelijk dat de voorbereide transactie uiteindelijk wordt afgerond. Ofwel door de gebruiker die het heeft gestart (of een andere gebruiker), of zelfs door een cron-script dat elke, laten we zeggen 30 minuten, controleert op vergeten transacties. Vergeten deze transactie te beëindigen zal catastrofale gevolgen hebben omdat het verhindert dat VACUUM wordt uitgevoerd, en natuurlijk zal de vergrendeling er nog steeds zijn, waardoor schrijven naar de database wordt voorkomen. Als u niet vertrouwd genoeg bent met uw systeem, als u niet alle aspecten en alle bijwerkingen van het gebruik van een voorbereide/verdeelde transactie met een slot volledig begrijpt, als u niet over voldoende toezicht beschikt, vooral met betrekking tot de MVCC metrieken, volg dan gewoon deze benadering niet. In dit geval zou u een speciale tabel kunnen hebben met parameters voor beheerdersdoeleinden, waarbij u twee speciale kolomwaarden zou kunnen gebruiken, één voor normale werking en één voor globale afgedwongen anonimisering, of u zou kunnen experimenteren met gedeelde adviesvergrendelingen op toepassingsniveau van PostgreSQL:

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

  1. Zijn PostgreSQL-kolomnamen hoofdlettergevoelig?

  2. Hoe maak en gebruik je een tijdelijke tabel in de Oracle-opgeslagen procedure?

  3. Toegang 2016 voor Dummies Cheatsheet

  4. Hoe EXCEPT werkt in PostgreSQL