2018年にGDPRが導入される前、および導入された後、ソフトウェアスタックのさまざまなレイヤーを使用するだけでなく、さまざまなアプローチを使用して、ユーザーデータの削除または非表示の問題を解決するための多くのアイデアがありました。 (ハード削除、ソフト削除、匿名化)。匿名化は、PostgreSQLベースの組織/企業の間で人気があることが知られているものの1つです。
GDPRの精神に基づき、企業間で交換されるビジネスドキュメントとレポートの要件がますます高まっているため、これらのレポートに表示される個人は匿名で表示されます。つまり、役割/役職のみが表示されます。 、個人データは非表示になっています。これはおそらく、これらのレポートを受け取った企業がGDPRの手順/プロセスの下でこれらのデータを管理することを望まず、それらを処理するための新しい手順/プロセス/システムを設計する負担に対処することを望まないという事実が原因です。 、そして彼らはすでに匿名化されたデータを受け取るように要求するだけです。したがって、この匿名化は、忘れられたいと表明した個人だけでなく、実際にはレポート内で言及されているすべての人々に適用されます。これは、一般的なGDPRの慣行とはまったく異なります。
この記事では、この問題の解決に向けた匿名化について説明します。まず、恒久的な解決策を提示します。これは、システム内の今後のすべての問い合わせで、忘れを要求している人を隠す必要がある解決策です。次に、これに基づいて、「オンデマンド」、つまり短期間の匿名化を実現する方法を紹介します。これは、必要なレポートがシステムで生成されるまで十分に長くアクティブになることを目的とした匿名化メカニズムの実装を意味します。私が提示しているソリューションでは、これはグローバルな効果をもたらすため、このソリューションは貪欲なアプローチを使用し、すべてのアプリケーションをカバーし、コードの書き直しを最小限に抑えます(そして、PostgreSQLDBAがそのような問題を一元的にアプリから離れる傾向にあります開発者は実際のワークロードに対処します)。ただし、ここで紹介する方法は、限定された/狭い範囲に適用するように簡単に調整できます。
ここでは、匿名化を実現する方法を紹介します。会社の従業員の記録を含む次の表を考えてみましょう。
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=#
このテーブルは公開されており、誰でもクエリを実行でき、公開スキーマに属しています。次に、匿名化の基本的なメカニズムを作成します。これは次の要素で構成されます。
- 関連するテーブルとビューを保持するための新しいスキーマ、これを匿名と呼びましょう
- 忘れたい人のIDを含むテーブル:anonym.person_anonym
- public.personの匿名化されたバージョンを提供するビュー:anonym.person
- 新しいビューを使用するための、search_pathの設定
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;
警告 :アプリケーションのデータソース定義でsearch_pathが正しく設定されていることが重要です。読者は、検索パスを処理するためのより高度な方法を検討することをお勧めします。より複雑で動的なロジックを処理できる関数を使用します。たとえば、データ入力ユーザー(またはロール)のセットを指定し、管理/レポートユーザーのセットを定義しながら、匿名化間隔全体で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=#
ここで、シン氏が会社を辞め、書面による声明によって忘れられる権利を明示的に表明したとします。アプリケーションは、彼のIDを「忘れられる」IDのセットに挿入することによってこれを行います:
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という名前のテーブルにはselectのみが許可されます。
- 以前の匿名化の設定に関係なく、すべてのアクセス(選択)はpersonテーブルのすべてのレコードに対して匿名化されます。
- 2フェーズコミット(別名、準備済みトランザクション)。
特別な管理アプリ(例:markStartOfAnynimizationPeriod)が実行されます
testdb=# BEGIN ;
BEGIN
testdb=# LOCK public.person IN SHARE MODE ;
LOCK TABLE
testdb=# PREPARE TRANSACTION 'personlock';
PREPARE TRANSACTION
testdb=#
上記の機能は、SHAREモードでテーブルのロックを取得して、INSERTS、UPDATES、DELETESがブロックされるようにすることです。また、2フェーズコミットトランザクション(分散トランザクションまたは拡張アーキテクチャトランザクションXAとして知られる他のコンテキストでは、準備されたトランザクション)を開始することにより、匿名化期間の開始をマークするセッションの接続からトランザクションを解放し、他の後続のセッションをその存在を認識しています。準備されたトランザクションは、(PREPARE TRANSACTIONを介して)開始した接続/セッションの切断後も存続する永続的なトランザクションです。 「PREPARETRANSACTION」ステートメントは、トランザクションと現在のセッションの関連付けを解除することに注意してください。準備されたトランザクションは、後続のセッションで取得され、ロールバックまたはコミットされます。この種のXAトランザクションを使用すると、システムは多くの異なるXAデータソースを確実に処理し、それらの(場合によっては異種の)データソース間でトランザクションロジックを実行できます。ただし、この特定のケースで使用する理由:
- 発行側のクライアントセッションでセッションを終了し、接続を切断/解放できるようにするには(接続を「維持」することは非常に悪い考えです。接続が実行されたらすぐに解放する必要があります)実行する必要のあるクエリ)
- 終了セッションがこの準備されたトランザクションを(その名前を使用して)コミットできるようにし、次のようにマークします:
- SHAREMODEロックの解放
匿名化期間の終了
トランザクションが有効であり、個人テーブルの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=#
上記のクエリは、指定された準備済みトランザクションのpersonlockが有効であり、実際にこの仮想トランザクションによって保持されているテーブルpersonの関連するロックが目的のモードである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=#
警告! :ロックは同時書き込みを防止しますが、ロックが解放されたときに最終的な書き込みを防止しません。そのため、アプリを更新し、データベースから「****」を読み取り、不注意なユーザーが更新を押して、しばらく待った後、SHAREDロックが解放され、更新が「***」の書き込みに成功する可能性があります。正しい通常のデータがあるべき場所の代わりに*'。もちろん、ユーザーはやみくもにボタンを押さないことでここを助けることができますが、いくつかの追加の保護をここに追加することができます。アプリを更新すると、次の問題が発生する可能性があります:
set lock_timeout TO 1;
アラーム! :準備されたトランザクションは最終的に完了する必要があります。それを開始したユーザー(または別のユーザー)によって、あるいは忘れられたトランザクションを30分ごとにチェックするcronスクリプトによってさえ。このトランザクションを終了するのを忘れると、VACUUMの実行が妨げられるため、壊滅的な結果が発生します。もちろん、ロックは引き続き存在し、データベースへの書き込みが妨げられます。システムに十分に慣れていない場合、ロック付きの準備/分散トランザクションを使用することのすべての側面とすべての副作用を完全に理解していない場合、特にMVCCに関して適切な監視が行われていない場合メトリック、そして単にこのアプローチに従わないでください。この場合、管理目的でパラメータを保持する特別なテーブルを作成し、1つは通常の操作用、もう1つはグローバルに強制される匿名化用の2つの特別な列値を使用するか、PostgreSQLアプリケーションレベルの共有アドバイザリロックを試すことができます。
- https://www.postgresql.org/docs/10/explicit-locking.html#ADVISORY-LOCKS
- https://www.postgresql.org/docs/10/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS