sql >> データベース >  >> RDS >> PostgreSQL

オンデマンドのPostgreSQL匿名化

    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;
    更新トランザクションの開始時。このように、1msより長い待機/ブロックは例外を発生させます。これは、大多数のケースから保護する必要があります。もう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

    1. SQLiteで外部キーを作成する

    2. インデックスの一部ではない外部キーの列に関するAndroidRoomのコンパイル時の警告。どういう意味ですか?

    3. GoogleBigQueryODBCドライバー

    4. SQLで文字列を連結する方法