sql >> Databáze >  >> RDS >> PostgreSQL

PostgreSQL anonymizace na vyžádání

Před, během a po příchodu GDPR v roce 2018 bylo mnoho nápadů, jak vyřešit problém mazání nebo skrývání uživatelských dat pomocí různých vrstev softwarové sady, ale také pomocí různých přístupů (tvrdé mazání, měkké mazání, anonymizace). Anonymizace je jedním z nich, o kterém je známo, že je populární mezi organizacemi/společnostmi založenými na PostgreSQL.

V duchu GDPR stále častěji vidíme požadavek na výměnu obchodních dokumentů a zpráv mezi společnostmi, takže jednotlivci uvedení v těchto zprávách jsou prezentováni anonymizováni, tj. je zobrazena pouze jejich role/název , přičemž jejich osobní údaje jsou skryté. Děje se tak s největší pravděpodobností proto, že společnosti, které obdrží tato hlášení, nechtějí tato data spravovat podle postupů/procesů GDPR, nechtějí se potýkat s břemenem navrhování nových postupů/procesů/systémů k jejich zpracování. a pouze žádají o obdržení již předem anonymizovaných dat. Tato anonymizace se tedy netýká pouze těch jednotlivců, kteří vyjádřili přání být zapomenuti, ale vlastně všech osob zmíněných v hlášení, což je zcela odlišné od běžných postupů GDPR.

V tomto článku se budeme zabývat anonymizací směrem k řešení tohoto problému. Začneme předložením trvalého řešení, tedy řešení, ve kterém by měl být člověk žádající o zapomenutí skryt při všech budoucích dotazech v systému. V návaznosti na to pak představíme způsob, jak dosáhnout „on demand“, tedy krátkodobé anonymizace, což znamená implementaci anonymizačního mechanismu, který má být aktivní jen tak dlouho, než se v systému vygenerují potřebné reporty. V řešení, které předkládám, to bude mít globální účinek, takže toto řešení používá zištný přístup, který pokrývá všechny aplikace, s minimálním (pokud vůbec nějakým) přepisováním kódu (a pochází z tendence PostgreSQL DBA řešit takové problémy centrálně opuštěním aplikace). vývojáři se zabývají svou skutečnou pracovní zátěží). Zde uvedené metody však lze snadno upravit tak, aby byly aplikovány v omezeném/užším rozsahu.

Trvalá anonymizace

Zde představíme způsob, jak dosáhnout anonymizace. Podívejme se na následující tabulku obsahující záznamy o zaměstnancích společnosti:

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

Tato tabulka je veřejná, každý se na ni může dotazovat a patří do veřejného schématu. Nyní vytvoříme základní mechanismus pro anonymizaci, který se skládá z:

  • nové schéma pro uchování souvisejících tabulek a zobrazení, říkejme tomu anonym
  • tabulka obsahující ID lidí, kteří chtějí být zapomenuti:anonym.person_anonym
  • zobrazení poskytující anonymizovanou verzi public.person:anonym.person
  • nastavení vyhledávací_cesty pro použití nového zobrazení
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
;

Nastavíme vyhledávací_cestu pro naši aplikaci:

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

Upozornění :je nezbytné, aby byla cesta hledání správně nastavena v definici zdroje dat v aplikaci. Čtenáři se doporučuje, aby prozkoumal pokročilejší způsoby zpracování vyhledávací cesty, např. s použitím funkce, která zvládne složitější a dynamickou logiku. Můžete například určit sadu uživatelů pro zadávání dat (nebo roli) a nechat je používat tabulku public.person po celý interval anonymizace (takže budou i nadále vidět normální data), a zároveň definovat sadu uživatelů pro manažery/přehledy. (nebo role), pro které bude platit logika anonymizace.

Nyní se zeptejme na náš osobní vztah:

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

Nyní předpokládejme, že pan Singh opustí společnost a písemným prohlášením výslovně vyjádří své právo být zapomenut. Aplikace to provede vložením jeho ID do sady ID „na zapomenutí“:

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

Zopakujme nyní přesně dotaz, který jsme spustili dříve:

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

Vidíme, že podrobnosti pana Singha nejsou z aplikace dostupné.

Dočasná globální anonymizace

Hlavní myšlenka

  • Uživatel označí začátek intervalu anonymizace (krátké časové období).
  • Během tohoto intervalu jsou pro tabulku s názvem osoby povoleny pouze výběry.
  • Veškerý přístup (výběr) je anonymizován pro všechny záznamy v tabulce osob, bez ohledu na předchozí nastavení anonymizace.
  • Uživatel označí konec intervalu anonymizace.

Stavební bloky

  • Dvoufázové potvrzení (neboli připravené transakce).
  • Explicitní zamykání tabulky.
  • Nastavení anonymizace, které jsme provedli výše v části „Trvalá anonymizace“.

Implementace

Provádí speciální aplikace pro správu (např. s názvem :markStartOfAnynimizationPeriod) 

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

Výše uvedené umožňuje získat zámek na stole v režimu SHARE, takže jsou zablokovány VLOŽENÍ, AKTUALIZACE, VYMAZÁNÍ. Také zahájením dvoufázové transakce potvrzení (AKA připravená transakce, v jiných kontextech známých jako distribuované transakce nebo transakce eXtended Architecture XA) osvobozujeme transakci od připojení relace označující začátek období anonymizace, zatímco ostatní následné relace mohou být spuštěny. vědom si své existence. Připravená transakce je trvalá transakce, která zůstává naživu i po odpojení spojení/relace, která ji zahájila (prostřednictvím PŘÍPRAVY TRANSAKCE). Všimněte si, že příkaz „PŘIPRAVIT TRANSAKCI“ odpojí transakci od aktuální relace. Připravená transakce může být vyzvednuta následnou relací a buď vrácena zpět, nebo potvrzena. Použití tohoto druhu transakcí XA umožňuje systému spolehlivě pracovat s mnoha různými zdroji dat XA a provádět transakční logiku napříč těmito (možná heterogenními) zdroji dat. Nicméně důvody, proč jej používáme v tomto konkrétním případě:

  • abyste umožnili relaci vydávajícího klienta ukončit relaci a odpojit/uvolnit její připojení (opuštění nebo ještě horší „přetrvávání“ připojení je opravdu špatný nápad, připojení by mělo být uvolněno, jakmile se provede dotazy, které musí provést)
  • aby se následné relace/spojení mohly dotazovat na existenci této připravené transakce
  • aby byla koncová relace schopná provést tuto připravenou transakci (pomocí jejího názvu), čímž se označí:
    • uvolnění zámku REŽIMU SDÍLENÍ
    • konec období anonymizace

Abychom ověřili, že transakce je aktuální a je spojena se zámkem SHARE na naší tabulce osob:

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

Výše uvedený dotaz slouží k tomu, aby zajistil, že pojmenovaná připravená transakce osobního zámku je aktivní a že přidružený zámek na stole osoby držené touto virtuální transakcí je skutečně v zamýšleném režimu:SHARE.

Takže nyní můžeme pohled upravit:

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

Nyní s novou definicí, pokud uživatel spustil připravený osobní zámek transakce, vrátí se následující výběr:

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

testdb=#

což znamená globální bezpodmínečnou anonymizaci.

Jakákoli aplikace, která se pokouší použít data od osoby v tabulce, získá místo skutečných skutečných dat anonymizované „****“. Nyní předpokládejme, že správce této aplikace rozhodne, že má skončit období anonymizace, takže jeho aplikace nyní vydá:

COMMIT PREPARED 'personlock';

Nyní se jakýkoli další výběr vrátí:

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

Upozornění! :Zámek zabraňuje souběžnému zápisu, ale nebrání případnému zápisu, když bude zámek uvolněn. Existuje tedy potenciální nebezpečí aktualizace aplikací, načtení '****' z databáze, neopatrného uživatele, stisknutí aktualizace a poté po nějaké době čekání se zámek SHARED uvolní a aktualizace úspěšně zapíše '*** *' na místě, kde by měla být správná normální data. Uživatelé zde samozřejmě mohou pomoci tím, že nebudou mačkat tlačítka naslepo, ale některé další ochrany by zde mohly být přidány. Aktualizace aplikací může způsobit:

set lock_timeout TO 1;

na začátku  aktualizace transakce. Tímto způsobem každé čekání/blokování delší než 1 ms vyvolá výjimku. Což by mělo chránit před drtivou většinou případů. Dalším způsobem by bylo kontrolní omezení v kterémkoli z citlivých polí pro kontrolu s hodnotou „****“.

ALARM! :je nutné, aby připravená transakce byla nakonec dokončena. Buď uživatelem, který to spustil (nebo jiným uživatelem), nebo dokonce cron skriptem, který kontroluje zapomenuté transakce každých řekněme 30 minut. Zapomenutí ukončit tuto transakci způsobí katastrofální výsledky, protože zabrání spuštění VACUUM a samozřejmě tam bude zámek, který zabrání zápisu do databáze. Pokud nejste dostatečně spokojeni se svým systémem, pokud plně nerozumíte všem aspektům a všem vedlejším účinkům použití připravené/distribuované transakce se zámkem, pokud nemáte zaveden odpovídající monitoring, zejména pokud jde o MVCC metriky, pak tento přístup jednoduše nedodržujte. V tomto případě byste mohli mít speciální parametry tabulky uchovávající pro účely správce, kde byste mohli použít dvě speciální hodnoty sloupců, jednu pro normální provoz a jednu pro globální vynucenou anonymizaci, nebo můžete experimentovat se sdílenými poradními zámky na úrovni aplikace 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. Implementace fulltextového vyhledávání v SQL Server 2016 pro pokročilé uživatele

  2. Zvýšení výkonu databáze o 400 %

  3. Použití Pythonu a MySQL v procesu ETL:SQLAlchemy

  4. Jak používat array_agg() pro varchar[]