Før, under og efter GDPR kom til byen i 2018, har der været mange ideer til at løse problemet med at slette eller skjule brugerdata ved at bruge forskellige lag af softwarestakken, men også ved at bruge forskellige tilgange (hård sletning, blød sletning, anonymisering). Anonymisering har været en af dem, som er kendt for at være populær blandt de PostgreSQL-baserede organisationer/virksomheder.
I GDPR's ånd ser vi mere og mere kravet om, at forretningsdokumenter og rapporter udveksles mellem virksomheder, således at de personer, der vises i disse rapporter, præsenteres anonymiseret, dvs. kun deres rolle/titel vises. , mens deres personlige data er skjult. Dette sker højst sandsynligt på grund af det faktum, at de virksomheder, der modtager disse rapporter, ikke ønsker at administrere disse data under procedurerne/processerne i GDPR, de ønsker ikke at håndtere byrden ved at designe nye procedurer/processer/systemer til at håndtere dem , og de beder bare om at modtage dataene, der allerede er præ-anonymiseret. Så denne anonymisering gælder ikke kun for de personer, der har udtrykt deres ønske om at blive glemt, men faktisk alle personer, der er nævnt i rapporten, hvilket er ret forskelligt fra den almindelige GDPR-praksis.
I denne artikel skal vi beskæftige os med anonymisering i retning af en løsning på dette problem. Vi starter med at præsentere en permanent løsning, det vil sige en løsning, hvor en person, der ønsker at blive glemt, skal skjules i alle fremtidige henvendelser i systemet. Ud over dette vil vi derefter præsentere en måde at opnå "on demand", dvs. kortvarig anonymisering, hvilket betyder implementering af en anonymiseringsmekanisme beregnet til at være aktiv lige længe nok, indtil de nødvendige rapporter genereres i systemet. I den løsning, jeg præsenterer, vil dette have en global effekt, så denne løsning bruger en grådig tilgang, der dækker alle applikationer, med minimal (hvis nogen) kodeomskrivning (og kommer fra tendensen hos PostgreSQL DBA'er til at løse sådanne problemer centralt forlader appen) udviklere håndterer deres sande arbejdsbyrde). De metoder, der præsenteres her, kan dog let tilpasses til at blive anvendt i begrænsede/snævrere omfang.
Permanent anonymisering
Her vil vi præsentere en måde at opnå anonymisering på. Lad os overveje følgende tabel, der indeholder optegnelser over en virksomheds ansatte:
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=#
Denne tabel er offentlig, alle kan forespørge på den og tilhører det offentlige skema. Nu skaber vi den grundlæggende mekanisme for anonymisering, som består af:
- et nyt skema til at indeholde relaterede tabeller og visninger, lad os kalde dette anonymt
- en tabel med id'er for personer, der ønsker at blive glemt:anonym.person_anonym
- en visning, der giver den anonymiserede version af public.person:anonym.person
- opsætning af søgestien for at bruge den nye visning
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
;
Lad os indstille søgestien til vores applikation:
set search_path = anonym,"$user", public;
Advarsel :det er vigtigt, at søgestien er konfigureret korrekt i datakildedefinitionen i applikationen. Læseren opfordres til at udforske mere avancerede måder at håndtere søgestien på, f.eks. med brug af en funktion, som kan håndtere mere kompleks og dynamisk logik. For eksempel kan du angive et sæt dataindtastningsbrugere (eller rolle) og lade dem fortsætte med at bruge public.person-tabellen gennem hele anonymiseringsintervallet (så de vil blive ved med at se normale data), mens de definerer et ledelses-/rapporteringssæt af brugere (eller rolle), for hvem anonymiseringslogikken vil gælde.
Lad os nu forespørge på vores personrelation:
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=#
Lad os nu antage, at hr. Singh forlader virksomheden og udtrykkeligt udtrykker sin ret til at blive glemt ved en skriftlig erklæring. Applikationen gør dette ved at indsætte hans id i sættet af "at blive glemt" id'er:
testdb=# insert into anonym.person_anonym (id) VALUES(1);
INSERT 0 1
Lad os nu gentage den nøjagtige forespørgsel, vi kører før:
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=#
Vi kan se, at hr. Singhs oplysninger ikke er tilgængelige fra applikationen.
Midlertidig global anonymisering
Hovedidéen
- Brugeren markerer starten på anonymiseringsintervallet (en kort periode).
- I dette interval er kun udvalgte tilladte for den navngivne person i tabellen.
- Al adgang (valg) er anonymiseret for alle poster i persontabellen, uanset tidligere anonymiseringsopsætning.
- Brugeren markerer slutningen af anonymiseringsintervallet.
Byggesten
- To-faset commit (også kendt som Forberedte Transaktioner).
- Eksplicit tabellåsning.
- Anonymiseringsopsætningen, vi lavede ovenfor i afsnittet "Permanent Anonymisering".
Implementering
En speciel admin-app (f.eks. kaldet:markStartOfAnynimizationPeriod) udfører
testdb=# BEGIN ;
BEGIN
testdb=# LOCK public.person IN SHARE MODE ;
LOCK TABLE
testdb=# PREPARE TRANSACTION 'personlock';
PREPARE TRANSACTION
testdb=#
Hvad ovenstående gør, er at få en lås på bordet i DEL-tilstand, så INDSÆT, OPDATERINGER, SLETTER blokeres. Også ved at starte en tofaset commit-transaktion (AKA forberedt transaktion, i andre sammenhænge kendt som distribuerede transaktioner eller eXtended Architecture-transaktioner XA) frigør vi transaktionen fra forbindelsen til sessionen, der markerer starten på anonymiseringsperioden, mens vi lader andre efterfølgende sessioner blive klar over dens eksistens. Den forberedte transaktion er en vedvarende transaktion, som forbliver i live efter afbrydelsen af forbindelsen/sessionen, som har startet den (via PREPARE TRANSACTION). Bemærk, at "PREPARE TRANSACTION"-erklæringen adskiller transaktionen fra den aktuelle session. Den forberedte transaktion kan afhentes ved en efterfølgende session og enten tilbageføres eller forpligtes. Brugen af denne type XA-transaktioner gør det muligt for et system pålideligt at håndtere mange forskellige XA-datakilder og udføre transaktionslogik på tværs af disse (muligvis heterogene) datakilder. Men grundene til at vi bruger det i dette specifikke tilfælde:
- for at gøre det muligt for den udstedende klientsession at afslutte sessionen og afbryde/frigøre forbindelsen (at forlade eller endnu værre "vedvare" en forbindelse er en rigtig dårlig idé, en forbindelse bør frigives, så snart den udfører de forespørgsler, den skal udføre)
- for at lave efterfølgende sessioner/forbindelser, der er i stand til at forespørge efter eksistensen af denne forberedte transaktion
- for at gøre den afsluttende session i stand til at udføre denne forberedte transaktion (ved brug af dens navn) og således markere:
- frigivelsen af SHARE MODE-låsen
- afslutningen af anonymiseringsperioden
For at verificere, at transaktionen er i live og forbundet med SHARE-låsen på vores personbord, gør vi:
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=#
Hvad ovenstående forespørgsel gør, er at sikre, at den navngivne forberedte transaktionspersonlås er i live, og at den tilknyttede bordlås, der holdes af denne virtuelle transaktion, faktisk er i den tilsigtede tilstand:DEL.
Så nu kan vi justere visningen:
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 med den nye definition, hvis brugeren har startet forberedt transaktionspersonlås, vil følgende valg returnere:
testdb=# select * from person;
id | givenname | midname | surname | address | email | role | rank
----+-----------+---------+---------+---------+-------+----------+-----------
1 | **** | **** | **** | **** | **** | Seafarer | Captain
2 | **** | **** | **** | **** | **** | IT | DBA
3 | **** | **** | **** | **** | **** | IT | Developer
(3 rows)
testdb=#
hvilket betyder global ubetinget anonymisering.
Enhver app, der forsøger at bruge data fra tabelperson, vil blive anonymiseret "****" i stedet for faktiske rigtige data. Lad os nu antage, at administratoren af denne app beslutter, at anonymiseringsperioden skal udløbe, så hans app udsender nu:
COMMIT PREPARED 'personlock';
Nu vil ethvert efterfølgende valg returnere:
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=#
Advarsel! :Låsen forhindrer samtidige skrivninger, men forhindrer ikke eventuel skrivning, når låsen vil være udløst. Så der er en potentiel fare for at opdatere apps, læse '****' fra databasen, en skødesløs bruger, trykke på opdatering, og så efter nogen tids ventetid frigives den DELEDE lås, og opdateringen lykkes med at skrive '*** *' i stedet for, hvor korrekte normale data skal være. Brugere kan selvfølgelig hjælpe her ved ikke at trykke blindt på knapper, men nogle ekstra beskyttelser kan tilføjes her. Opdatering af apps kan give et:
set lock_timeout TO 1;
i starten af opdateringstransaktionen. På denne måde vil enhver ventning/blokering længere end 1 ms medføre en undtagelse. Hvilket skal beskytte mod langt de fleste tilfælde. En anden måde ville være en kontrolbegrænsning i et hvilket som helst af de følsomme felter for at kontrollere mod '****'-værdien.
ALARM! :det er bydende nødvendigt, at den forberedte transaktion til sidst skal gennemføres. Enten af brugeren, der startede det (eller en anden bruger), eller endda af et cron-script, der tjekker for glemte transaktioner hvert lad os sige 30 minutter. At glemme at afslutte denne transaktion vil forårsage katastrofale resultater, da det forhindrer VACUUM i at køre, og låsen vil selvfølgelig stadig være der, hvilket forhindrer skrivning til databasen. Hvis du ikke er komfortabel nok med dit system, hvis du ikke fuldt ud forstår alle aspekter og alle bivirkninger ved at bruge en forberedt/distribueret transaktion med en lås, hvis du ikke har tilstrækkelig overvågning på plads, især med hensyn til MVCC målinger, så følg simpelthen ikke denne tilgang. I dette tilfælde kan du have en speciel tabel med parametre til administrationsformål, hvor du kan bruge to specielle kolonneværdier, en til normal drift og en til global tvungen anonymisering, eller du kan eksperimentere med PostgreSQL delte rådgivende låse på applikationsniveau:
- https://www.postgresql.org/docs/10/explicit-locking.html#ADVISORY-LOCKS
- https://www.postgresql.org/docs/10/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS