PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

PostgreSQL анонимизация при поискване

Преди, по време и след като GDPR влезе в града през 2018 г., имаше много идеи за решаване на проблема с изтриването или скриването на потребителски данни, като се използват различни слоеве от софтуерния стек, но също така се използват различни подходи (твърдо изтриване, меко изтриване, анонимизиране). Анонимизацията е една от тях, за която е известно, че е популярна сред базираните на PostgreSQL организации/компании.

В духа на GDPR виждаме все повече изискване за обмен на бизнес документи и отчети между компании, така че лицата, показани в тези отчети, се представят анонимно, т.е. показва се само тяхната роля/титул , докато личните им данни са скрити. Това се случва най-вероятно поради факта, че тези компании, които получават тези отчети, не искат да управляват тези данни съгласно процедурите/процесите на GDPR, те не искат да се справят с тежестта на проектиране на нови процедури/процеси/системи за обработката им , и те просто искат да получат данните, които вече са предварително анонимни. Така че тази анонимизация не се отнася само за тези лица, които са изразили желанието си да бъдат забравени, но всъщност за всички хора, споменати в доклада, което е доста различно от обичайните практики на GDPR.

В тази статия ще се занимаваме с анонимизирането за решение на този проблем. Ще започнем с представяне на постоянно решение, тоест решение, при което човек, който иска да бъде забравен, трябва да бъде скрит при всички бъдещи запитвания в системата. След това надграждайки това, ще представим начин за постигане на „при поискване“, т.е. краткотрайна анонимизация, което означава прилагане на механизъм за анонимизиране, предназначен да бъде активен достатъчно дълго, докато необходимите отчети се генерират в системата. В решението, което представям, това ще има глобален ефект, така че това решение използва алчен подход, обхващащ всички приложения, с минимално (ако има такова) пренаписване на код (и идва от тенденцията на PostgreSQL DBA да решават такива проблеми централно напускайки приложението разработчиците се справят с истинското си работно натоварване). Въпреки това, представените тук методи могат лесно да бъдат настроени, за да се прилагат в ограничени/по-тесни обхвати.

Постоянна анонимизация

Тук ще ви представим начин за постигане на анонимизация. Нека разгледаме следната таблица, съдържаща записи на служителите на компанията:

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

Тази таблица е публична, всеки може да я запитва и принадлежи към публичната схема. Сега създаваме основния механизъм за анонимизиране, който се състои от:

  • нова схема за съхраняване на свързани таблици и изгледи, нека наречем това анонимно
  • таблица, съдържаща идентификатори на хора, които искат да бъдат забравени:anonym.person_anonym
  • изглед, предоставящ анонимната версия на public.person:anonym.person
  • настройка на пътя_търсене, за да използвате новия изглед
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
;

Нека зададем search_path към нашето приложение:

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

Предупреждение :важно е пътят за търсене да е настроен правилно в дефиницията на източника на данни в приложението. Читателят се насърчава да изследва по-усъвършенствани начини за справяне с пътя за търсене, напр. с използване на функция, която може да обработва по-сложна и динамична логика. Например можете да посочите набор от потребители за въвеждане на данни (или роля) и да им позволите да продължат да използват таблицата public.person през целия интервал на анонимизация (така че те ще продължат да виждат нормални данни), като същевременно дефинирате набор от потребители за управление/отчитане (или роля), за които ще се прилага логиката на анонимизация.

Сега нека да запитаме връзката ни с личността:

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

Сега да предположим, че г-н Сингх напуска компанията и изрично изразява правото си да бъде забравен с писмено изявление. Приложението прави това, като вмъква неговия идентификатор в набора от идентификационни номера „да бъде забравен“:

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

Нека сега повторим точната заявка, която изпълнявахме преди:

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

Можем да видим, че данните на г-н Сингх не са достъпни от приложението.

Временна глобална анонимизация

Основната идея

  • Потребителят маркира началото на интервала за анонимизиране (кратък период от време).
  • През този интервал са разрешени само избори за таблицата с име person.
  • Целият достъп (избирания) са анонимизирани за всички записи в таблицата с хора, независимо от предходните настройки за анонимизиране.
  • Потребителят маркира края на интервала за анонимизиране.

Строителни блокове

  • Двуфазен комит (известен още като подготвени транзакции).
  • Изрично заключване на таблицата.
  • Настройката за анонимизиране, която направихме по-горе в секцията „Постоянна анонимизация“.

Внедряване

Специално приложение за администратор (напр. наречено:markStartOfAnynimizationPeriod) изпълнява 

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

Това, което прави горното, е заключване на таблицата в режим СПОДЕЛЯНЕ, така че ВМЪКВАНЕ, АКТУАЛИЗИРАНЕ, ИЗТРИВАНЕ са блокирани. Също така чрез стартиране на двуфазна транзакция за ангажимент (AKA подготвена транзакция, в други контексти, известни като разпределени транзакции или транзакции с разширена архитектура XA), ние освобождаваме транзакцията от връзката на сесията, отбелязваща началото на периода на анонимизация, като оставяме други последващи сесии да бъдат наясно с съществуването му. Подготвената транзакция е постоянна транзакция, която остава жива след прекъсването на връзката/сесията, която я е започнала (чрез ПОДГОТВИТЕ ТРАНЗАКЦИЯ). Обърнете внимание, че операторът „PREPARE TRANSACTION“ разделя транзакцията от текущата сесия. Подготвената транзакция може да бъде взета от следваща сесия и да бъде връщана назад или ангажиментирана. Използването на този вид XA транзакции позволява на системата надеждно да се справя с много различни източници на XA данни и да изпълнява транзакционна логика в тези (евентуално хетерогенни) източници на данни. Въпреки това, причините, поради които го използваме в този конкретен случай:

  • за да се даде възможност на издаващата клиентска сесия за прекратяване на сесията и прекъсване/освобождаване на връзката й (напускането или още по-лошото „продължаване“ на връзка е наистина лоша идея, връзката трябва да бъде освободена веднага щом се изпълни заявките, които трябва да направи)
  • за да направите следващите сесии/връзки способни да запитват за съществуването на тази подготвена транзакция
  • за да направи крайната сесия способна да извърши тази подготвена транзакция (чрез използването на нейното име), като по този начин маркирате:
    • освобождаването на заключването на РЕЖИМ НА СПОДЕЛЯНЕ
    • края на периода на анонимизиране

За да се уверим, че транзакцията е жива и е свързана със заключването SHARE на нашата таблица с хора, правим:

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

Това, което прави заявката по-горе, е да гарантира, че наименуваната подготвена персонална блокировка за транзакция е жива и че действително свързаното заключване на лице на таблицата, държано от тази виртуална транзакция, е в предвидения режим:SHARE.

Сега можем да настроим изгледа:

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

Сега с новата дефиниция, ако потребителят е започнал подготвена транзакция personlock, тогава следният избор ще върне:

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

testdb=#

което означава глобална безусловна анонимизация.

Всяко приложение, което се опитва да използва данни от човек в таблицата, ще получи анонимизиран „****“ вместо действителни реални данни. Сега да предположим, че администраторът на това приложение решава, че периодът на анонимизация трябва да приключи, така че приложението му сега издава:

COMMIT PREPARED 'personlock';

Сега всеки следващ избор ще върне:

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

Предупреждение! :Заключването предотвратява едновременното записване, но не предотвратява евентуалното записване, когато заключването ще бъде освободено. Така че има потенциална опасност от актуализиране на приложения, четене на „****“ от базата данни, невнимателен потребител, натискане на актуализация и след това след известен период на изчакване, СПЕДЕЛНОТО заключване се освобождава и актуализацията успява да изпише „*** *“ на мястото на правилните нормални данни. Потребителите, разбира се, могат да помогнат тук, като не натискат сляпо бутони, но тук могат да бъдат добавени някои допълнителни защити. Актуализирането на приложения може да доведе до:

set lock_timeout TO 1;

в началото  на транзакцията за актуализиране. По този начин всяко чакане/блокиране по-дълго от 1 мс ще предизвика изключение. Което трябва да предпазва от по-голямата част от случаите. Друг начин би било ограничение за проверка в някое от чувствителните полета за проверка спрямо стойността „****“.

АЛАРМА! :наложително е подготвената транзакция в крайна сметка да бъде завършена. Или от потребителя, който го е стартирал (или друг потребител), или дори от cron скрипт, който проверява за забравени транзакции на всеки да кажем 30 минути. Забравянето за прекратяване на тази транзакция ще доведе до катастрофални резултати, тъй като не позволява на VACUUM да работи и, разбира се, заключването все още ще бъде там, предотвратявайки записите в базата данни. Ако не сте достатъчно удобни с вашата система, ако не разбирате напълно всички аспекти и всички странични ефекти от използването на подготвена/разпределена транзакция със заключване, ако нямате адекватен мониторинг, особено по отношение на MVCC метрики, тогава просто не следвайте този подход. В този случай бихте могли да имате специална таблица, съдържаща параметри за административни цели, където бихте могли да използвате две специални стойности на колони, една за нормална работа и една за глобална принудителна анонимизация, или можете да експериментирате със споделени съветни заключвания на ниво приложение на 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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да конфигурирате PostgreSQL да приема всички входящи връзки

  2. PostgreSQL връща набор от резултати като JSON масив?

  3. ПОРЪЧАЙТЕ ПО списъка със стойности IN

  4. Използване на регулярен израз в WHERE в Postgres

  5. Повече SQL, по-малко код, с PostgreSQL