

# Aurora PostgreSQL での動的マスキングの使用
<a name="AuroraPostgreSQL.Security.DynamicMasking"></a>

動的データマスキングは、クエリ時にデータがどのようにユーザーに表示されるかを制御することで、Aurora PostgreSQL データベース内の機密データを保護するセキュリティ機能です。Aurora は、`pg_columnmask` 拡張機能を通じて実装します。`pg_columnmask` は、PostgreSQL のネイティブな行レベルセキュリティときめ細かなアクセスコントロールメカニズムを補完する列レベルのデータ保護を提供します。

`pg_columnmask` では、ユーザーロールに基づいてデータの可視性を決定するマスキングポリシーを作成します。ユーザーがマスキングポリシーを使用してテーブルをクエリする場合、Aurora PostgreSQL はユーザーのロールとポリシーの重みに基づいて、クエリ時に適切なマスキング関数を適用します。基盤となるデータはストレージ内で変更されずに残ります。

`pg_columnmask` は、次の機能をサポートしています。
+ **組み込みおよびカスタムマスキング関数** – E メールやテキストのマスキングなどの一般的なパターンに構築済みの関数を使用するか、独自のカスタム関数を作成して SQL ベースのマスキングポリシーを通じて機密データ (PII) を保護します。
+ **複数のマスキング戦略** – 情報を完全に非表示にしたり、部分的な値をワイルドカードに置き換えたり、カスタムマスキングアプローチを定義したりできます。
+ **ポリシーの優先順位付け** – 1 つの列に複数のポリシーを定義します。重みを使用して、列に複数のポリシーが適用されるときに使用するマスキングポリシーを決定します。Aurora PostgreSQL は、重みとユーザーロールメンバーシップに基づいてポリシーを適用します。

`pg_columnmask` は、Aurora PostgreSQL バージョン 16.10 以降、およびバージョン 17.6 以降で使用できます。追加料金なしで利用できます。

# 動的マスキングの開始方法
<a name="AuroraPostgreSQL.Security.DynamicMasking.GetStarted"></a>

データを動的にマスクするには、データベースに `pg_columnmask` 拡張機能をインストールし、テーブルのマスキングポリシーを作成します。セットアッププロセスには、前提条件の検証、拡張機能のインストール、ロール設定、ポリシーの作成、検証テストが含まれます。

## 拡張機能のインストールと設定
<a name="AuroraPostgreSQL.Security.DynamicMasking.GetStarted.Installation"></a>

RDS コンソールクエリエディタまたは psql などの PostgreSQL クライアントと rds\$1superuser (マスターユーザー) 認証情報を使用して、Aurora PostgreSQL クラスターに接続します。

拡張機能作成コマンドを実行して `pg_columnmask` 機能を有効にします。

```
CREATE EXTENSION pg_columnmask;
```

このコマンドは、`pg_columnmask` 拡張機能をインストールし、必要なカタログテーブルを作成し、組み込みのマスキング関数を登録します。拡張機能のインストールはデータベース固有です。つまり、機能が必要なデータベースごとに個別にインストールする必要があります。

**注記**  
この拡張機能をインストールする前に行われた接続には、マスクされていないデータが表示されます。閉じて再接続し、これを修正します。

使用可能なマスキング関数を確認して、拡張機能のインストールを確認します。

```
SELECT proname FROM pg_proc
    WHERE pronamespace = 'pgcolumnmask'::regnamespace AND proname LIKE 'mask_%';
    proname     
--------Output --------
 mask_email
 mask_text
 mask_timestamp
(3 rows)
```

# データマスキングポリシーを管理する手順
<a name="AuroraPostgreSQL.Security.DynamicMasking.Procedures"></a>

`pg_columnmask` 拡張機能が提供する手順を使用して、マスキングポリシーを管理できます。マスキングポリシーを作成、変更、または削除するには、次のいずれかの権限が必要です。
+ `pg_columnmask` ポリシーを作成するテーブルの所有者。
+ `rds_superuser` のメンバー。
+ `pgcolumnmask.policy_admin_rolname` パラメータによって設定された `pg_columnmask` ポリシーマネージャーロールのメンバー。

次のコマンドは、以降のセクションで使用するテーブルを作成します。

```
CREATE TABLE public.customers (
    id SERIAL PRIMARY KEY,
    name TEXT,
    phone TEXT,
    address TEXT,
    email TEXT
);
```

## CREATE\$1MASKING\$1POLICY
<a name="AuroraPostgreSQL.Security.DynamicMasking.Procedures.CreateMaskingPolicy"></a>

次の手順では、ユーザーテーブルの新しいマスキングポリシーを作成します。

**構文**

```
create_masking_policy(
    policy_name,
    table_name,
    masking_expressions,
    roles,
    weight)
```

**引数**


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| policy\$1name | NAME |  マスキングポリシーの名前。テーブルごとに一意である必要があります。  | 
| table\$1name | REGCLASS |  マスキングポリシーを適用するテーブルの修飾名または非修飾名または oid。  | 
| masking\$1expressions | JSONB |  列名とマスキング関数のペアを含む JSON オブジェクト。各キーは列名で、その値はその列に適用されるマスキング式です。  | 
| roles | NAME[] |  このマスキングポリシーが適用されるロール。デフォルトは PUBLIC です。  | 
| weight | INT |  マスキングポリシーの重み。特定のユーザーのクエリに複数のポリシーが適用される場合、重みが最も大きいポリシー (整数数が高い) がマスクされた各列に適用されます。 デフォルトは 0 です。テーブル上の 2 つのマスキングポリシーが同じ重みを持つことはできません。  | 

**戻り値の型**:

なし

**Example `test_user` ロールの E メール列をマスクするマスキングポリシーの作成:**  

```
CALL pgcolumnmask.create_masking_policy(
    'customer_mask',
    'public.customers',
    JSON_OBJECT('{
        "email", "pgcolumnmask.mask_email(email)"
    }')::JSONB,
    ARRAY['test_user'],
    100
);
```

## ALTER\$1MASKING\$1POLICY
<a name="AuroraPostgreSQL.Security.DynamicMasking.Procedures.AlterMaskingPolicy"></a>

この手順では、既存のマスキングポリシーを変更します。`ALTER_MASKING_POLICY` は、ポリシーのマスキング式、ポリシーが適用されるロールのセット、およびマスキングポリシーの重みを変更できます。これらのパラメータのいずれかを省略すると、ポリシーの対応する部分は変更されません。

**構文**

```
alter_masking_policy(
    policy_name,
    table_name,
    masking_expressions,
    roles,
    weight)
```

**引数**


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| policy\$1name | NAME |  既存のマスキングポリシーの名前。  | 
| table\$1name | REGCLASS |  マスキングポリシーを含むテーブルの修飾/非修飾名 oid。  | 
| masking\$1expressions | JSONB |  列名とマスキング関数ペアまたは NULL を含む新しい JSON オブジェクト。  | 
| roles | NAME[] |  このマスキングポリシーが適用される新しいロールのリスト。それ以外の場合は NULL。  | 
| weight | INT |  マスキングポリシーまたは NULL の新しい重み。  | 

**戻り値の型**:

なし

**Example 他のポリシー属性を変更せずに、アナリストロールを既存のマスキングポリシーに追加する。**  

```
CALL pgcolumnmask.alter_masking_policy(
    'customer_mask',
    'public.customers',
    NULL,
    ARRAY['test_user', 'analyst'],
    NULL 
);

-- Alter the weight of the policy without altering other details
CALL pgcolumnmask.alter_masking_policy(
    'customer_mask',
    'customers',
    NULL,
    NULL,
    4
);
```

## DROP\$1MASKING\$1POLICY
<a name="AuroraPostgreSQL.Security.DynamicMasking.Procedures.DropMaskingPolicy"></a>

この手順では、既存のマスキングポリシーを削除します。

**構文**

```
drop_masking_policy(
        policy_name,
        table_name)
```

**引数**


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| policy\$1name | NAME |  既存のマスキングポリシーの名前。  | 
| table\$1name | REGCLASS |  マスキングポリシーを含むテーブルの修飾/非修飾名 oid。  | 

**戻り値の型**:

なし

**Example マスキングポリシー customer\$1mask の削除**  

```
-- Drop a masking policy
    CALL pgcolumnmask.drop_masking_policy(
        'customer_mask',
        'public.customers',
    );
```

# マスキングポリシー DDL プロシージャでの識別子のエスケープ
<a name="AuroraPostgreSQL.Security.DynamicMasking.EscapeIdentifiers"></a>

引用符で囲まれた識別子を使用してデータマスキングポリシーを作成する場合、正しいオブジェクト参照とポリシーアプリケーションを確保するために、適切なエスケープが必要です。`pg_columnmask` マスキングポリシー管理手順内で引用符で囲まれた識別子を使用するには:
+ **ポリシー名** – 二重引用符で囲む必要があります。
+ **テーブル名** – スキーマ名とテーブル名は、必要に応じて二重引用符で個別に囲む必要があります。
+ **マスキング式** – マスキング式の列名と関数名は二重引用符で囲む必要があり、引用符自体はバックスラッシュを使用してエスケープする必要があります。
+ **ロール** – ロール名の配列は自動的に引用符で囲まれます。ロール名は、大文字と小文字の区別を含めて、「`pg_roles`」に示されている名前と完全に一致する必要があります。

**Example エスケープ構文と引用構文**  
この例では、Aurora PostgreSQL で大文字と小文字が混在する名前を使用する、または引用符で囲まれた識別子を必要とするテーブル、列、関数、ロールのマスキングポリシーを作成する際における、適切なエスケープ構文と引用構文を示します。  

```
-- Create a table and columns with mixed case name 
CREATE TABLE public."Employees" (
    "Name" TEXT,
    "Email" TEXT,
    ssn VARCHAR(20)
);

-- Create a role with mixed case name
CREATE ROLE "Masked_user";

-- Create a function with mixed case name
CREATE OR REPLACE FUNCTION public."MaskEmail"(text)
    RETURNS character varying
    LANGUAGE plpgsql
    IMMUTABLE PARALLEL SAFE
    AS $$ BEGIN
        RETURN 'XXXXXXXX'::text;
    END $$;

-- Now use these objects with mixed case names in
-- masking policy management procedures
CALL pgcolumnmask.create_masking_policy(
    '"Policy1"',  -- policy name should be surrounded with double quotes for quoting
    'public."Employees"', -- table and schema name should be individually 
                          -- surrounded with double quotes for quoting
    JSON_OBJECT('{
        "\"Email\"", "\"MaskEmail\"(\"Email\")"
    }')::JSONB, -- masking expression should have double quotes around function names
                -- and columns names etc when needed. Also the double quotes itself
                -- should be escaped using \ (backslash) since this is a JSON string
    ARRAY['Masked_user'], -- Rolename do not need quoting
                          -- (this behaviour may change in future release)
    100
);

SELECT * FROM pgcolumnmask.pg_columnmask_policies
    WHERE tablename = 'Employees';
-[ RECORD 1 ]-----+-------------------------------------
schemaname        | public
tablename         | Employees
policyname        | Policy1
roles             | {Masked_user}
masked_columns    | {Email}
masking_functions | {"(\"MaskEmail\"(\"Email\"))::text"}
weight            | 100
```

## 管理ビュー
<a name="AuroraPostgreSQL.Security.DynamicMasking.AdminViews"></a>

パブリックにアクセス可能な `pgcolumnmask.pg_columnmask_policies` 管理ビューを使用して、すべての `pg_columnmask` ポリシーを確認できます。このビューでは、次の情報を使用できます。ビューは、現在のユーザーが所有するマスキングポリシーのみを返します。


| 列名 | データ型 | 説明 | 
| --- | --- | --- | 
|  schemaname  | NAME |  ポリシーがアタッチされているリレーションのスキーマ  | 
|  tablename  | NAME |  ポリシーがアタッチされているリレーションの名前  | 
|  policyname  | NAME |  マスキングポリシーの名前。すべてのマスキングポリシーには一意の名前があります。  | 
|  ロール  | TEXT[] |  ポリシーが適用されるロール。  | 
|  masked\$1columns  | TEXT[] |  マスクされた列  | 
|  masking\$1functions  | TEXT[] |  マスキング関数  | 
| weight | INT |  アタッチされたポリシーの重み  | 

# 事前定義されたデータマスキング関数
<a name="AuroraPostgreSQL.Security.DynamicMasking.PredefinedMaskingFunctions"></a>

`pg_columnmask` 拡張機能は、`pg_columnmask` ポリシーのマスキング式として使用できる C 言語 (高速実行用) で記述された組み込みユーティリティ関数を提供します。

**mask\$1text**

設定可能な可視性オプションを使用してテキストデータをマスクする関数。

**引数**:


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| input | TEXT |  マスクする元のテキスト文字列  | 
| mask\$1char | CHAR(1) |  マスキングに使用される文字 (デフォルト: 'X')  | 
| visible\$1prefix | INT |  入力テキストの先頭でマスクされていないままになる文字数 (デフォルト: 0)  | 
| visible\$1suffix | INT |  マスクされていないままになる入力テキストの末尾の文字数 (デフォルト: 0)  | 
| use\$1hash\$1mask | BOOLEAN |  TRUE の場合、mask\$1char の代わりにハッシュベースのマスキングを使用します (デフォルト: FALSE)  | 

**Example さまざまなマスキングオプションの使用**  
入力文字列全体をデフォルトの「X」文字でマスクする  

```
postgres=> SELECT pgcolumnmask.mask_text('Hello World');
  mask_text  
-------------
 XXXXXXXXXXX
```
`mask_char` 引数を使用して、別の文字を使用してテキスト入力をマスクする  

```
postgres=> SELECT pgcolumnmask.mask_text('Hello World', '*');
  mask_text  
-------------
 ***********
```
`visible_prefix` および `visible_suffix` パラメータを使用して、テキストの先頭と末尾でマスクされない文字数を制御します。  

```
postgres=> SELECT pgcolumnmask.mask_text('Hello World', '*', 5, 1);
  mask_text  
-------------
 Hello*****d
```
`use_hash_mask` が true の場合、入力文字列はランダムな文字を使用してマスクされます。`mask_char` 引数は無視されますが、`visible_prefix` と `visible_suffix` は引き続き有効です。  

```
postgres=> SELECT pgcolumnmask.mask_text('Hello World', '*', 2, 2, true);
  mask_text  
-------------
 Hex36dOHild
```

**mask\$1timestamp**


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| ts\$1to\$1mask | TIMESTAMP |  マスクする元のタイムスタンプ  | 
| mask\$1part | TEXT |  タイムスタンプのどの部分をマスクするかを指定します (デフォルト: 'all') 有効な値: 'year'、'month'、'day'、'hour'、'minute'、'second'、'all'  | 
| mask\$1value | TIMESTAMP |  マスキングに使用するタイムスタンプ値 (デフォルト: '1900-01-01 00:00:00')  | 

**Example `mask_timestamps` の使用**  
これらの例は、デフォルト値への完全なタイムスタンプのマスキング、特定のタイムスタンプコンポーネント (年のみ) の部分的なマスキング、カスタム置換値によるマスキングを示しています。  
入力値をデフォルトのタイムスタンプに完全にマスクする  

```
postgres=> SELECT pgcolumnmask.mask_timestamp('2023-06-15 14:30:00');
   mask_timestamp    
---------------------
 1900-01-01 00:00:00
```
年のみの例からタイムスタンプの一部のみをマスクするには  

```
postgres=> SELECT pgcolumnmask.mask_timestamp('2023-06-15 14:30:00', 'year');
   mask_timestamp    
---------------------
 1900-06-15 14:30:00
```
タイムスタンプのマスクされた値を変更するには、`mask_value` 引数を使用します。  

```
postgres=> SELECT pgcolumnmask.mask_timestamp('2023-06-15 14:30:00', 'all', '2012-12-12 12:12:12');
   mask_timestamp    
---------------------
 2012-12-12 12:12:12
```

**mask\$1timestamp**

E メール構造を保持しながら E メールアドレスをマスクする関数。


| パラメータ | Datatype | 説明 | 
| --- | --- | --- | 
| input | TEXT |  マスクする元の E メールアドレス  | 
| mask\$1char | CHAR(1) |  マスキングに使用される文字 (デフォルト: 'X')  | 
| mask\$1local | BOOLEAN |  TRUE の場合、E メールのローカル部分をマスクします (@ より前) (デフォルト: TRUE)  | 
| mask\$1domain | BOOLEAN |  TRUE の場合、E メールのドメイン部分をマスクします (@ の後) (デフォルト: TRUE)  | 

**Example `mask_email` の使用**  
これらの例は、E メールアドレスのローカル部分またはドメイン部分の完全な E メールマスキング、カスタムマスク文字、選択的マスキングを示しています。  
完全なマスキング  

```
postgres=> SELECT pgcolumnmask.mask_email('user@example.com');
    mask_email    
------------------
 XXXX@XXXXXXX.com
```
マスキングに使用する文字を変更するには、`mask_char` を使用します。  

```
postgres=> SELECT pgcolumnmask.mask_email('user@example.com', '*');
    mask_email    
------------------
 ****@*******.com
```
`mask_local` と `mask_domain` を使用してローカルとドメインのマスキングを制御する  

```
postgres=> SELECT pgcolumnmask.mask_email('user@example.com', '*', true, false);
    mask_email    
------------------
 ****@example.com

postgres=> SELECT pgcolumnmask.mask_email('user@example.com', '*', false, true);
    mask_email    
------------------
 user@*******.com
```

# エンドツーエンドのワークフローでの pg\$1columnmask の実装
<a name="AuroraPostgreSQL.Security.DynamicMasking.WorkflowExample"></a>

このセクションでは、機密データを含むサンプル従業員テーブル `pg_columnmask` を使用するための完全な実装を示します。カスタムマスキング関数を作成し、さまざまなロール (内部、サポート、アナリスト) の異なる重みレベルを持つ複数のマスキングポリシーを定義し、単一または複数のロールメンバーシップを持つユーザーがさまざまなレベルのマスクされたデータを表示する方法を確認します。この例では、RETURNING 句を使用した DML ステートメントのマスキング動作、テーブルとビューのトリガー、名前の変更、重みの変更、クリーンアップなどのポリシー管理オペレーションについても説明します。

1. 機密データを含むサンプルテーブルを作成します。

   ```
   CREATE SCHEMA hr;
   
   CREATE TABLE hr.employees (
       id INT PRIMARY KEY,
       name TEXT NOT NULL,
       email TEXT,
       ssn TEXT,
       salary NUMERIC(10,2)
    );
   
   INSERT INTO hr.employees VALUES
       (1, 'John Doe', 'john.doe@example.com', '123-45-6789', 50000.00),
       (2, 'Jane Smith', 'jane.smith@example.com', '987-65-4321', 60000.00);
   ```

1. カスタムマスキング関数を作成します。

   ```
   CREATE OR REPLACE FUNCTION public.mask_ssn(ssn TEXT)
       RETURNS TEXT AS $$
       BEGIN
           RETURN 'XXX-XX-' || RIGHT(ssn, 4);
       END;
       $$ LANGUAGE plpgsql;
   
   CREATE OR REPLACE FUNCTION public.mask_salary(salary NUMERIC, multiplier NUMERIC DEFAULT 0.0)
       RETURNS NUMERIC AS $$
       BEGIN
           RETURN salary * multiplier;
       END;
       $$ LANGUAGE plpgsql;
   ```

1. ユーザーロールに基づいて異なるマスキングレベルで複数のポリシーを作成します。

   ```
   -- Create different roles
   CREATE ROLE analyst_role;
   CREATE ROLE support_role;
   CREATE ROLE intern_role;
   
   GRANT USAGE ON SCHEMA hr TO analyst_role, support_role, intern_role;
   GRANT SELECT ON hr.employees TO analyst_role, support_role, intern_role;
   ----------------------------------------------------------------------
   
   -- Low-Weight Policy (Intern)
   CALL pgcolumnmask.create_masking_policy(
       'employee_mask_strict',
       'hr.employees',
       JSON_BUILD_OBJECT('name', 'pgcolumnmask.mask_text(name, ''*'')',
                         'email', 'pgcolumnmask.mask_email(email)',
                         'ssn', 'pgcolumnmask.mask_text(ssn, ''*'')',
                         'salary', 'public.mask_salary(salary)')::JSONB,
       ARRAY['intern_role'],
       10  -- Lowest weight
   );
   
   ----------------------------------------------------------------------
   -- Medium-Weight Policy (Support)
   CALL pgcolumnmask.create_masking_policy(
       'employee_mask_moderate',
       'hr.employees',
       JSON_BUILD_OBJECT('email', 'pgcolumnmask.mask_email(email, ''#'')',
                         'ssn', 'public.mask_ssn(ssn)',
                         'salary', 'public.mask_salary(salary)')::JSONB,
       ARRAY['support_role'],
       50   -- Medium weight
   );
   
   ----------------------------------------------------------------------
   -- High-Weight Policy (Analyst)
   CALL pgcolumnmask.create_masking_policy(
       'employee_mask_light',
       'hr.employees',
       JSON_BUILD_OBJECT('ssn', 'public.mask_ssn(ssn)',
                         'salary', 'public.mask_salary(salary, 0.9)')::JSONB,
       ARRAY['analyst_role'],
       100   -- Highest weight
   );
   ```

1. 次の例は、ロールメンバーシップとポリシーの重みに基づいてさまざまなユーザーがデータを表示する方法を示しています。

   ```
   -- Create users
   CREATE USER sarah_intern;
   GRANT intern_role TO sarah_intern;
   
   CREATE USER lisa_support;
   GRANT support_role TO lisa_support;
   
   CREATE USER mike_analyst;
   GRANT analyst_role TO mike_analyst;
   
   CREATE USER ethan_support_intern;
   GRANT support_role, intern_role TO ethan_support_intern;
   
   CREATE USER john_analyst_intern;
   GRANT analyst_role, intern_role TO john_analyst_intern;
   ```

   インターンとして (最も厳格なマスキング):

   ```
   SET ROLE sarah_intern;
   
   SELECT * FROM hr.employees;
    id |    name    |         email          |     ssn     | salary 
   ----+------------+------------------------+-------------+--------
     1 | ********   | XXXXXXXX@XXXXXXX.com   | *********** |   0.00
     2 | ********** | XXXXXXXXXX@XXXXXXX.com | *********** |   0.00
   ```

   サポートユーザーとして (中程度のマスキング):

   ```
   SET ROLE lisa_support;
   
   SELECT * FROM hr.employees;
    id |    name    |         email          |     ssn     | salary 
   ----+------------+------------------------+-------------+--------
     1 | John Doe   | ########@#######.com   | XXX-XX-6789 |   0.00
     2 | Jane Smith | ##########@#######.com | XXX-XX-4321 |   0.00
   ```

   アナリストとして (最小のマスキング):

   ```
   SET ROLE mike_analyst;
   
   SELECT * FROM hr.employees;
    id |    name    |         email          |     ssn     |  salary  
   ----+------------+------------------------+-------------+----------
     1 | John Doe   | john.doe@example.com   | XXX-XX-6789 | 45000.00
     2 | Jane Smith | jane.smith@example.com | XXX-XX-4321 | 54000.00
   ```

   インターンユーザーとサポートユーザーの両方である ethan\$1support\$1intern ユーザーとして:

   ```
   SET ROLE ethan_support_intern;
   
   -- masking policies appliable to this user: employee_mask_strict and employee_mask_moderate
   -- id : unmasked because no masking policy appliable on ethan_support_intern
   --            masks these columns
   -- name : masked because of employee_mask_strict policy
   -- email, ssn, salary : both employee_mask_strict and employee_mask_moderate mask these columns
   --                      but employee_mask_moderate will be use because of higher weight 
   
   SELECT * FROM hr.employees;
    id |    name    |         email          |     ssn     | salary 
   ----+------------+------------------------+-------------+--------
     1 | ********   | ########@#######.com   | XXX-XX-6789 |   0.00
     2 | ********** | ##########@#######.com | XXX-XX-4321 |   0.00
   ```

   インターンとアナリストの両方である john\$1analyst\$1intern として:

   ```
   SET ROLE john_analyst_intern;
   
   -- masking policies appliable to this user: employee_mask_strict and employee_mask_light
   -- id : unmasked because no masking policy appliable on john_analyst_intern
   --            masks these columns
   -- name, email : masked because of employee_mask_strict
   -- ssn, salary : both employee_mask_strict and employee_mask_light mask these columns
   --               but employee_mask_light will be use because of higher weight 
   
   SELECT * FROM hr.employees;
    id |    name    |         email          |     ssn     |  salary  
   ----+------------+------------------------+-------------+----------
     1 | ********   | XXXXXXXX@XXXXXXX.com   | XXX-XX-6789 | 45000.00
     2 | ********** | XXXXXXXXXX@XXXXXXX.com | XXX-XX-4321 | 54000.00
   ```

# DML オペレーションのマスキング動作について
<a name="AuroraPostgreSQL.Security.DynamicMasking.DMLMasking"></a>

`pg_columnmask` は、INSERT、UPDATE、DELETE、MERGE ステートメントを含むすべての DML オペレーションに一貫して適用されます。これらのオペレーションを実行すると、Aurora PostgreSQL はコア原則に従ってデータをマスクします。ストレージから読み取られたデータは、現在のユーザーの適用可能なポリシーに従ってマスクされます。

マスキングは、次のようなクエリコンポーネントの一部に影響します。
+ WHERE 句
+ JOIN 条件
+ サブクエリ
+ RETURNING 句

これらのコンポーネントはすべて、元のデータではなくマスクされた値で動作します。データはマスクされていない状態でストレージに書き込まれますが、ユーザーが読み返すときにはマスクされたビューのみが表示されます。

Aurora PostgreSQL は、マスクされた値ではなく、実際の保存値に対してすべてのデータベース制約 (NOT NULL、UNIQUE、CHECK、FOREIGN KEY) を適用します。これにより、マスキング関数が慎重に設計されていない場合に、明らかな不整合が生じることがあります。

マスキングは列レベルのアクセス許可とともに機能します。
+ SELECT 権限のないユーザーは列を読み取ることができません
+ SELECT 権限を持つユーザーは、該当するポリシーに従ってマスクされた値を表示します。

# トリガー関数のマスキング動作について
<a name="AuroraPostgreSQL.Security.DynamicMasking.TriggerFunctionMasking"></a>

`pg_columnmask` ポリシーをテーブルに適用するときは、マスキングがトリガー関数とどのように相互作用するかを理解することが重要です。トリガーは、INSERT、UPDATE、DELETE オペレーションなど、テーブル上の特定のイベントに応答して自動的に実行されるデータベース関数です。

デフォルトでは、DDM はトリガーのタイプに応じて異なるマスキングルールを適用します。

テーブルトリガー  
**移行テーブルのマスク解除** — テーブルのトリガー関数は、古い行バージョンと新しい行バージョンの両方で、移行テーブルのマスク解除されたデータにアクセスできます。  
テーブル所有者はトリガーを作成し、データを所有するため、テーブルを効果的に管理するためのフルアクセスを持ちます

ビュートリガー (トリガーの代わりに)  
**移行テーブルがマスクされている** – ビューのトリガー関数は、現在のユーザーのアクセス許可に従ってマスクされたデータを参照します  
ビュー所有者はベーステーブル所有者とは異なる場合があり、基盤となるテーブルのマスキングポリシーを尊重する必要があります

2 つのサーバーレベルの設定パラメータは、マスクされたテーブルでトリガーの動作を制御します。これらは `rds_superuser` によってのみ設定できます。
+ **マスクされたテーブルのトリガーを制限する** – マスクされたユーザーが、該当するマスキングポリシーを持つテーブルで DML オペレーションを実行するときにトリガーの実行を防止します。
+ **マスクされたテーブルを持つビューのトリガーを制限する:** – ビュー定義に現在のユーザーに適用されるマスキングポリシーを持つテーブルが含まれている場合、ビューでのトリガー実行を防止します。

**Example 関数アプリケーションとテーブルおよびビューの違い**  
次の例では、古い行の値と新しい行の値を出力するトリガー関数を作成し、同じ関数をテーブルにアタッチした場合とビューにアタッチした場合の動作がどのように異なるかを示します。  

```
-- Create trigger function
CREATE OR REPLACE FUNCTION print_changes()
    RETURNS TRIGGER AS
    $$
        BEGIN
        RAISE NOTICE 'Old row: name=%, email=%, ssn=%, salary=%',
            OLD.name, OLD.email, OLD.ssn, OLD.salary;
        
        RAISE NOTICE 'New row: name=%, email=%, ssn=%, salary=%',
            NEW.name, NEW.email, NEW.ssn, NEW.salary;
        
        RETURN NEW;
        END;
    $$ LANGUAGE plpgsql;

-- Create trigger
CREATE TRIGGER print_changes_trigger
    BEFORE UPDATE ON hr.employees
    FOR EACH ROW
    EXECUTE FUNCTION print_changes();

-- Grant update to analyst role
GRANT UPDATE ON hr.employees TO analyst_role;

-- Unmasked data must be seen inside trigger even for masked user for the OLD and NEW
-- row passed to trigger function
BEGIN;
SET ROLE mike_analyst;
UPDATE hr.employees SET id = id + 10 RETURNING *;
NOTICE:  Old row: name=John Doe, email=john.doe@example.com, ssn=123-45-6789, salary=50000.00
NOTICE:  New row: name=John Doe, email=john.doe@example.com, ssn=123-45-6789, salary=50000.00
NOTICE:  Old row: name=Jane Smith, email=jane.smith@example.com, ssn=987-65-4321, salary=60000.00
NOTICE:  New row: name=Jane Smith, email=jane.smith@example.com, ssn=987-65-4321, salary=60000.00
 id |    name    |         email          |     ssn     |  salary  
----+------------+------------------------+-------------+----------
 11 | John Doe   | john.doe@example.com   | XXX-XX-6789 | 45000.00
 12 | Jane Smith | jane.smith@example.com | XXX-XX-4321 | 54000.00
(2 rows)

ROLLBACK;


-- Triggers on views (which are supposed to see masked data for new/old row)
CREATE VIEW hr.view_over_employees AS SELECT * FROM hr.employees;
GRANT UPDATE, SELECT ON hr.view_over_employees TO analyst_role;

-- Create trigger for this view
CREATE TRIGGER print_changes_trigger
    INSTEAD OF UPDATE ON hr.view_over_employees
    FOR EACH ROW
    EXECUTE FUNCTION print_changes();

-- Masked new and old rows should be passed to trigger if trigger is on view
BEGIN;
SET ROLE mike_analyst;
UPDATE hr.view_over_employees SET id = id + 10 RETURNING *;
NOTICE:  Old row: name=John Doe, email=john.doe@example.com, ssn=XXX-XX-6789, salary=45000.00
NOTICE:  New row: name=John Doe, email=john.doe@example.com, ssn=XXX-XX-6789, salary=45000.00
NOTICE:  Old row: name=Jane Smith, email=jane.smith@example.com, ssn=XXX-XX-4321, salary=54000.00
NOTICE:  New row: name=Jane Smith, email=jane.smith@example.com, ssn=XXX-XX-4321, salary=54000.00
 id |    name    |         email          |     ssn     |  salary  
----+------------+------------------------+-------------+----------
 11 | John Doe   | john.doe@example.com   | XXX-XX-6789 | 45000.00
 12 | Jane Smith | jane.smith@example.com | XXX-XX-4321 | 54000.00
(2 rows)
ROLLBACK;
```
マスクされたテーブルにトリガーを実装する前に、トリガーの動作を確認することをお勧めします。テーブルトリガーは移行テーブルのマスクされていないデータにアクセスできますが、ビュートリガーにはマスクされたデータが表示されます。

**Example マスキングポリシーの名前変更**  
次の例は、`rename_masking_policy` プロシージャを使用して既存のポリシーの名前を変更する方法を示しています。  

```
-- Rename the strict policy
CALL pgcolumnmask.rename_masking_policy(
    'employee_mask_strict',
    'hr.employees',
    'intern_protection_policy'
);

-- Verify the rename
SELECT policyname, roles, weight
    FROM pgcolumnmask.pg_columnmask_policies
    WHERE tablename = 'employees'
    ORDER BY weight DESC;

        policyname        |     roles      | weight 
--------------------------+----------------+--------
 employee_mask_light      | {analyst_role} |    100
 employee_mask_moderate   | {support_role} |     50
 intern_protection_policy | {intern_role}  |     10
```

**Example ポリシーの重みの変更**  
次の例は、ポリシーの重みを変更して重みを変える方法を示しています。  

```
-- Change weight of moderate policy
CALL pgcolumnmask.alter_masking_policy(
    'employee_mask_moderate'::NAME,
    'hr.employees'::REGCLASS,
    NULL,    -- Keep existing masking expressions
    NULL,    -- Keep existing roles
    75       -- New weight
);

-- Verify the changes
SELECT policyname, roles, weight
    FROM pgcolumnmask.pg_columnmask_policies
    WHERE tablename = 'employees'
    ORDER BY weight DESC;
        policyname        |     roles      | weight 
--------------------------+----------------+--------
 employee_mask_light      | {analyst_role} |    100
 employee_mask_moderate   | {support_role} |     75
 intern_protection_policy | {intern_role}  |     10
```

**Example クリーンアップ**  
次の例は、すべてのポリシー、テーブル、ユーザーを削除する方法を示しています。  

```
-- Drop policies
CALL pgcolumnmask.drop_masking_policy(
    'intern_protection_policy',
    'hr.employees'
);

CALL pgcolumnmask.drop_masking_policy(
    'employee_mask_moderate',
    'hr.employees'
);

CALL pgcolumnmask.drop_masking_policy(
    'employee_mask_light',
    'hr.employees'
);

-- Drop table and functions
DROP VIEW IF EXISTS hr.view_over_employees;
DROP TABLE IF EXISTS hr.employees;
DROP SCHEMA IF EXISTS hr;
DROP FUNCTION IF EXISTS public.mask_ssn(text);
DROP FUNCTION IF EXISTS public.mask_salary(numeric, numeric);

-- Drop users
DROP USER sarah_intern, lisa_support, mike_analyst,
    ethan_support_intern, john_analyst_intern;
DROP ROLE intern_role, support_role, analyst_role;
```

# マスキングポリシー管理ロールの設定
<a name="AuroraPostgreSQL.Security.DynamicMasking.PolicyManagementRole"></a>

PostgreSQL 列マスキング拡張機能 を使用すると、`pg_columnmask` または `rds_superuser` テーブル所有者の権限を必要とせずに、マスキングポリシーの管理を特定のロールに委任できます。これにより、マスキングポリシーを作成、変更、削除できるユーザーをより詳細に制御できます。

マスキングポリシー管理権限を持つロールを設定するには、次の手順に従います。

1. ポリシー管理者ロールの作成 – `rds_superuser` として、マスキングポリシーの管理を担当する新しいロールを作成します。

   ```
   CREATE ROLE mask_admin NOLOGIN;
   ```

1. PostgreSQL パラメータの設定 – カスタム DB クラスターパラメータグループで、`pgcolumnmask.policy_admin_rolname` エンジン設定パラメータを作成したロールの名前に設定します。

   ```
   pgcolumnmask.policy_admin_rolname = mask_admin
   ```

   このエンジン設定パラメータは DB クラスターパラメータグループで設定でき、インスタンスの再起動は必要ありません。パラメータの更新の詳細については、「[Amazon Aurora での DB クラスターパラメータグループのパラメータの変更](USER_WorkingWithParamGroups.ModifyingCluster.md)」を参照してください。

1. ユーザーにロールを付与する `rds_superuser` として、マスキングポリシーを管理できるユーザーに `mask_admin` ロールを付与します。

   ```
   CREATE USER alice LOGIN;
   CREATE USER bob LOGIN;
   GRANT mask_admin TO alice, bob;
   ```

   さらに、マスキングポリシーを管理するスキーマに対する USAGE 権限がユーザーに付与されていることを確認します。

   ```
   GRANT USAGE ON SCHEMA hr TO alice, bob;
   ```

これで、ユーザー `alice` と `bob` がデータベースに接続すると、標準の `pg_columnmask` 拡張機能を使用して、スキーマに対する `USAGE` 権限を持つすべてのスキーマ内のすべてのテーブルでマスキングポリシーを作成、変更、削除できるようになります。

# 安全な pg\$1columnmask 実装のベストプラクティス
<a name="AuroraPostgreSQL.Security.DynamicMasking.BestPractices"></a>

次のセクションでは、Aurora PostgreSQL 環境で `pg_columnmask` を実装するためのセキュリティのベストプラクティスについて説明します。次の推奨事項に従ってください。
+ 安全なロールベースのアクセスコントロールアーキテクチャを確立する
+ セキュリティの脆弱性を防ぐマスキング関数を開発する
+ マスクされたデータを使用してトリガーの動作を理解して制御する

## ロールベースのセキュリティアーキテクチャ
<a name="AuroraPostgreSQL.Security.DynamicMasking.BestPractices.architecture"></a>

データベースにアクセス制御を実装するロール階層を定義します。Aurora PostgreSQL `pg_columnmask` は、これらのロール内できめ細かなデータマスキング用のレイヤーを追加することで、これらのコントロールを強化します。

個々のユーザーにアクセス許可を付与するのではなく、組織の機能に沿った専用ロールを作成します。このアプローチにより、監査性が向上し、組織構造が進化するにつれてアクセス許可の管理が簡素化されます。

**Example 組織ロール階層の作成**  
次の例では、さまざまな機能専用のロールを持つ組織ロール階層を作成し、個々のユーザーを適切なロールに割り当てます。この例では、組織ロール (analyst\$1role、support\$1role) が最初に作成され、個々のユーザーにこれらのロールのメンバーシップが付与されます。この構造により、個々のユーザーではなくロールレベルでアクセス許可を管理できます。  

```
-- Create organizational role hierarchy
CREATE ROLE data_admin_role;
CREATE ROLE security_admin_role;
CREATE ROLE analyst_role;
CREATE ROLE support_role;
CREATE ROLE developer_role;

-- Specify security_admin_role as masking policy manager in the DB cluster parameter
-- group pgcolumnmask.policy_admin_rolname = 'security_admin_role'

-- Create specific users and assign to appropriate roles
CREATE USER security_manager;
CREATE USER data_analyst1, data_analyst2;
CREATE USER support_agent1, support_agent2;

GRANT security_admin_role TO security_manager;
GRANT analyst_role TO data_analyst1, data_analyst2;
GRANT support_role TO support_agent1, support_agent2;
```
各ロールに必要な最小限のアクセス許可のみを付与することで、最小特権の原則を実装します。認証情報が侵害された場合に悪用される可能性のある広範なアクセス許可を付与することは避けてください。  

```
-- Grant specific table permissions rather than schema-wide access
GRANT SELECT ON sensitive_data.customers TO analyst_role;
GRANT SELECT ON sensitive_data.transactions TO analyst_role;
-- Do not grant: GRANT ALL ON SCHEMA sensitive_data TO analyst_role;
```
ポリシー管理者には、マスキングポリシーを管理するスキーマに対する `USAGE` 権限が必要です。最小特権の原則に従って、これらの権限を選択的に付与します。スキーマアクセス許可を定期的に見直して、承認された担当者のみがポリシー管理機能を維持できるようにします。  
ポリシー管理者ロールのパラメータ設定は、データベース管理者のみに制限されます。このパラメータをデータベースレベルまたはセッションレベルで変更することはできません。これにより、権限のないユーザーがポリシー管理者割り当てを上書きできなくなります。この制限により、マスキングポリシーの制御が一元化され、安全に維持されます。  
ポリシー管理者ロールをグループではなく特定の個人に割り当てます。このターゲットを絞ったアプローチにより、ポリシー管理者はデータベース内のすべてのテーブルをマスクできるため、マスキングポリシー管理への選択的なアクセスが保証されます。

## 安全なマスキング関数の開発
<a name="AuroraPostgreSQL.Security.DynamicMasking.BestPractices.MaskingDevelopment"></a>

早期バインディングセマンティクスを使用してマスキング関数を開発し、適切な依存関係の追跡を確保し、ランタイム中の検索パスの変更などの遅延バインディングの脆弱性を防止します。SQL 関数の `BEGIN ATOMIC` 構文を使用して、コンパイル時の検証 (早期バインディング) と依存関係管理を有効にすることをお勧めします。

```
-- Example - Secure masking function with early binding
CREATE OR REPLACE FUNCTION secure_mask_ssn(input_ssn TEXT)
    RETURNS TEXT
    LANGUAGE SQL
    IMMUTABLE PARALLEL SAFE STRICT
    BEGIN ATOMIC
        SELECT CASE
            WHEN input_ssn IS NULL THEN NULL
            WHEN length(input_ssn) < 4 THEN repeat('X', length(input_ssn))
            ELSE repeat('X', length(input_ssn) - 4) || right(input_ssn, 4)
        END;
    END;
```

または、すべてのオブジェクト参照を明示的にスキーマで修飾することで、検索パスの変更を許容しない関数を作成し、異なるユーザーセッション間で一貫した動作を確保します。

```
-- Function immune to search path changes
CREATE OR REPLACE FUNCTION data_masking.secure_phone_mask(phone_number TEXT)
    RETURNS TEXT
    LANGUAGE SQL
    IMMUTABLE PARALLEL SAFE STRICT
    AS $$
    SELECT CASE
        WHEN phone_number IS NULL THEN NULL
        WHEN public.length(public.regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX'
        ELSE public.regexp_replace(
            phone_number,
            '([0-9]{3})[0-9]{3}([0-9]{4})',
            public.concat('\1-XXX-\2')
        )
    END;
    $$;
```

マスキング関数内に入力検証を実装してエッジケースを処理し、予期しない動作を防止します。常に NULL 処理を含め、入力形式を検証して、一貫したマスキング動作を確保します。

```
-- Robust masking function with comprehensive input validation
CREATE OR REPLACE FUNCTION secure_mask_phone(phone_number TEXT)
    RETURNS TEXT
    LANGUAGE SQL
    IMMUTABLE PARALLEL SAFE STRICT
    BEGIN ATOMIC
        SELECT CASE
            WHEN phone_number IS NULL THEN NULL
            WHEN length(trim(phone_number)) = 0 THEN phone_number
            WHEN length(regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX'
            ELSE regexp_replace(phone_number, '([0-9]{3})[0-9]{3}([0-9]{4})', '\1-XXX-\2')
        END;
    END;
```

## pg\$1columnmask を使用した DML トリガーの動作
<a name="AuroraPostgreSQL.Security.DynamicMasking.BestPractices.DMLTriggerBehavior"></a>

テーブルトリガーの場合、移行テーブルは完全にマスク解除されます。ビュートリガー (IOT) の場合、移行テーブルは現在のユーザーのビューアクセス許可に従ってマスクされます。

pg\$1columnmask を使用したテーブルトリガー  
トリガーには、DML クエリの実行によって変更された行の古いバージョンと新しいバージョンを含む移行テーブルが渡されます。トリガーがいつ起動されるかに応じて、Aurora PostgreSQL は古い行と新しい行を入力します。例えば、参照する古いバージョンがないため、`BEFORE INSERT` トリガーには新しいバージョンの行と空の古いバージョンのみが含まれます。  
`pg_columnmask` は、テーブルのトリガー内の移行テーブルをマスクしません。トリガーは、マスクされた列を本文内で使用でき、マスクされていないデータが表示されます。トリガー作成者は、トリガーがユーザーに対してどのように実行されるかを確認する必要があります。この場合、次の例は正しく動作します。  

```
-- Example for table trigger uses masked column in its definition
-- Create a table and insert some rows
CREATE TABLE public.credit_card_table (
    name TEXT,
    credit_card_no VARCHAR(16),
    is_fraud BOOL
);

INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud)
    VALUES
    ('John Doe', '4532015112830366', false),
    ('Jane Smith', '5410000000000000', true),
    ('Brad Smith', '1234567891234567', true);

-- Create a role which will see masked data and grant it privileges
CREATE ROLE intern_user;
GRANT SELECT, DELETE ON public.credit_card_table TO intern_user;

-- Trigger which will silenty skip delete of non fraudelent credit cards
CREATE OR REPLACE FUNCTION prevent_non_fraud_delete()
    RETURNS TRIGGER AS
    $$
    BEGIN
        IF OLD.is_fraud = false THEN
            RETURN NULL;
        END IF;
        RETURN OLD;
    END;
    $$ LANGUAGE plpgsql;

CREATE TRIGGER prevent_non_fraud_delete
    BEFORE DELETE ON credit_card_table
    FOR EACH ROW
    EXECUTE FUNCTION prevent_non_fraud_delete();

CREATE OR REPLACE FUNCTION public.return_false()
    RETURNS BOOLEAN
    LANGUAGE SQL
    IMMUTABLE PARALLEL SAFE STRICT
    BEGIN ATOMIC
      SELECT false;
    END;

-- A masking policy that masks both credit card number and is_fraud column.
-- If we apply masking inside trigger then prevent_non_fraud_delete trigger will
-- allow deleting more rows to masked user (even non fraud ones).
CALL pgcolumnmask.create_masking_policy(
    'mask_credit_card_no_&_is_fraud'::NAME,
    'public.credit_card_table'::REGCLASS,
    JSON_BUILD_OBJECT('credit_card_no', 'pgcolumnmask.mask_text(credit_card_no)',
                      'is_fraud', 'public.return_false()')::JSONB,
    ARRAY['intern_user']::NAME[],
    10::INT
);

-- Test trigger behaviour using intern_user
BEGIN;
SET ROLE intern_user;
-- credit card number & is_fraud is completely masked from intern_user
SELECT * FROM public.credit_card_table;
    name    |  credit_card_no  | is_fraud 
------------+------------------+----------
 John Doe   | XXXXXXXXXXXXXXXX | f
 Jane Smith | XXXXXXXXXXXXXXXX | f
 Brad Smith | XXXXXXXXXXXXXXXX | f
(3 rows)

-- The delete trigger lets the intern user delete rows for Jane and Brad even though
-- intern_user sees their is_fraud = false, but the table trigger works with original
-- unmasked value
DELETE FROM public.credit_card_table RETURNING *;
    name    |  credit_card_no  | is_fraud 
------------+------------------+----------
 Jane Smith | XXXXXXXXXXXXXXXX | f
 Brad Smith | XXXXXXXXXXXXXXXX | f
(2 rows)

COMMIT;
```
トリガー作成者は、ユーザーがトリガー本文で使用するステートメントに気を付けないと、マスクされていないデータをユーザーに漏洩してしまいます。例えば、`RAISE NOTICE ‘%’, masked_column;` を使用すると、現在のユーザーに列が出力されます。  

```
-- Example showing table trigger leaking column value to current user
CREATE OR REPLACE FUNCTION leaky_trigger_func()
    RETURNS TRIGGER AS
    $$
    BEGIN
        RAISE NOTICE 'Old credit card number was: %', OLD.credit_card_no;
        RAISE NOTICE 'New credit card number is %', NEW.credit_card_no;
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;

CREATE TRIGGER leaky_trigger
    AFTER UPDATE ON public.credit_card_table
    FOR EACH ROW
    EXECUTE FUNCTION leaky_trigger_func();

-- Grant update on column is_fraud to auditor role
-- auditor will NOT HAVE PERMISSION TO READ DATA
CREATE ROLE auditor;
GRANT UPDATE (is_fraud) ON public.credit_card_table TO auditor;

-- Also add auditor role to existing masking policy on credit card table
CALL pgcolumnmask.alter_masking_policy(
    'mask_credit_card_no_&_is_fraud'::NAME,
    'public.credit_card_table'::REGCLASS,
    NULL::JSONB,
    ARRAY['intern_user', 'auditor']::NAME[],
    NULL::INT
);

-- Log in as auditor
-- [auditor]
-- Update will fail if trying to read data from the table
UPDATE public.credit_card_table
    SET is_fraud = true
    WHERE credit_card_no = '4532015112830366';
ERROR:  permission denied for table cc_table

-- [auditor]
-- But leaky update trigger will still print the entire row even though
-- current user does not have permission to select from public.credit_card_table
UPDATE public.credit_card_table SET is_fraud = true;
NOTICE:  Old credit_card_no was: 4532015112830366
NOTICE:  New credit_card_no is 4532015112830366
```

(トリガーの代わりに) pg\$1columnmask を使用したビューでのトリガー  
トリガーは PostgreSQL のビューでのみ作成できます。更新できないビューで DML ステートメントを実行するために使用されます。ビュークエリ内で使用されるビューとベーステーブルの所有者が異なる可能性があるため、トランジットテーブルは常にトリガー (IOT) ではなく内部でマスクされます。この場合、ベーステーブルにはビュー所有者に適用されるマスキングポリシーがあり、ビュー所有者は常にトリガー内のベーステーブルからのマスクされたデータを表示する必要があります。これは、テーブルのトリガーとは異なります。この場合、トリガー作成者とテーブル内のデータは、ここでは存在しない同じユーザーによって所有されるためです。  

```
-- Create a view over credit card table
CREATE OR REPLACE VIEW public.credit_card_view
    AS
    SELECT * FROM public.credit_card_table;

-- Truncate credit card table and insert fresh data
TRUNCATE TABLE public.credit_card_table;
INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud)
    VALUES
    ('John Doe', '4532015112830366', false),
    ('Jane Smith', '5410000000000000', true),
    ('Brad Smith', '1234567891234567', true);

CREATE OR REPLACE FUNCTION public.print_changes()
    RETURNS TRIGGER AS
    $$
    BEGIN
        RAISE NOTICE 'Old row: name=%, credit card number=%, is fraud=%',
            OLD.name, OLD.credit_card_no, OLD.is_fraud;
    
        RAISE NOTICE 'New row: name=%, credit card number=%, is fraud=%',
            NEW.name, NEW.credit_card_no, NEW.is_fraud;
    
    RETURN NEW;
   END;
   $$ LANGUAGE plpgsql;

CREATE TRIGGER print_changes_trigger
    INSTEAD OF UPDATE ON public.credit_card_view
    FOR EACH ROW
    EXECUTE FUNCTION public.print_changes();

GRANT SELECT, UPDATE ON public.credit_card_view TO auditor;

-- [auditor]
-- Login as auditor role
BEGIN;

-- Any data coming out from the table will be masked in instead of triggers
-- according to masking policies applicable to current user
UPDATE public.credit_card_view
    SET name = CONCAT(name, '_new_name')
    RETURNING *;
NOTICE:  Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=John Doe_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=Jane Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=Brad Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
        name         |  credit_card_no  | is_fraud 
---------------------+------------------+----------
 John Doe_new_name   | XXXXXXXXXXXXXXXX | f
 Jane Smith_new_name | XXXXXXXXXXXXXXXX | f
 Brad Smith_new_name | XXXXXXXXXXXXXXXX | f
 
 -- Any new data going into the table using INSERT or UPDATE command will be unmasked
 UPDATE public.credit_card_view
    SET credit_card_no = '9876987698769876'
    RETURNING *;
NOTICE:  Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=John Doe, credit card number=9876987698769876, is fraud=f
NOTICE:  Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=Jane Smith, credit card number=9876987698769876, is fraud=f
NOTICE:  Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f
NOTICE:  New row: name=Brad Smith, credit card number=9876987698769876, is fraud=f
    name    |  credit_card_no  | is_fraud 
------------+------------------+----------
 John Doe   | 9876987698769876 | f
 Jane Smith | 9876987698769876 | f
 Brad Smith | 9876987698769876 | f
 
 COMMIT;
```

トリガーの動作を制御するデータベース/ユーザーレベルの GuC  
2 つの設定パラメータは、該当するマスキングポリシーを持つユーザーのトリガー実行動作を制御します。これらのパラメータを使用して、追加のセキュリティ制限が必要な場合に、マスクされたテーブルまたはビューでトリガーが実行されないようにします。両方のパラメータはデフォルトで無効になっているため、トリガーが正常に実行されます。  
**最初の GUC: マスクされたテーブルの発射制限をトリガーする**  
仕様:  
+ 名前: `pgcolumnmask.restrict_dml_triggers_for_masked_users`
+ 型: `boolean`
+ デフォルト: `false` (トリガーの実行が許可されます)
TRUE に設定すると、マスクされたユーザーのマスクされたテーブルでトリガーが実行されるのを防ぎます。`pg_columnmask` はエラーを実行します。  
**2 番目の GUC: マスクされたテーブルを持つビューで射撃制限をトリガーする**  
仕様:  
+ 名前: `pgcolumnmask.restrict_iot_triggers_for_masked_users`
+ 型: `boolean`
+ デフォルト: `false` (トリガーの実行が許可されます)
TRUE に設定すると、マスクされたユーザーの定義にマスクされたテーブルを含むビューでトリガーの実行が禁止されます。

これらのパラメータは独立して動作し、標準のデータベース設定パラメータのように設定できます。

# Aurora PostgreSQL pg\$1columnmask データ移動シナリオ
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement"></a>

`pg_columnmask` の動作は、オペレーションがストレージ、論理、またはアプリケーションレイヤーのいずれで行われるかに応じて、データ移動オペレーションごとに異なります。ストレージレベルのオペレーション (クローン作成など) の動作は、論理オペレーション (`pg_dump` など) やアプリケーションレベルのオペレーション (FDW クエリなど) とは異なります。このセクションでは、レプリケーション、バックアップ、エクスポート、移行などの一般的なシナリオのマスキング動作と、それぞれのセキュリティへの影響について説明します。

**Topics**
+ [Aurora Global Database とリードレプリカ](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.RR)
+ [データベースクローンとスナップショットの復元](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.Clones)
+ [論理レプリケーション](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.LogRep)
+ [Blue/Green デプロイ](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.BlueGreen)
+ [ゼロ ETL ストリームと CDC ストリーム](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.ZETL)
+ [AWS Database Migration Service](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DMS)
+ [データエクスポート](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DataExport)
+ [ビューとマテリアライズドビュー](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.Views)
+ [データダンプと復元](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DDR)
+ [外部データラッパー](#AuroraPostgreSQL.Security.DynamicMasking.DataMovement.FDQ)

## Aurora Global Database とリードレプリカ
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.RR"></a>

Aurora `pg_columnmask` ポリシーは、クラスターボリューム内のデータベースシステムテーブルに保存されます。すべてのレプリカが同じポリシーにアクセスし、一貫してマスクされた結果を返します。Aurora Global Database デプロイの場合、`pg_columnmask` ポリシーは他のデータベースシステムテーブルとともにセカンダリ AWS リージョンにレプリケートされるため、リージョン間で一貫したデータ保護が保証されます。フェイルオーバーシナリオ中、すべての `pg_columnmask` ポリシーはそのまま機能します。

## データベースクローンとスナップショットの復元
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.Clones"></a>

Aurora Fast Clone およびスナップショット復元オペレーションでは、すべての `pg_columnmask` ポリシー、ロール、および設定がデータベースシステムテーブルの一部として保持されます。クローンまたは復元されたデータベースは、ソースクラスターから既存のすべてのポリシーを継承します。クローン作成または復元後、各データベースクラスターは独立した `pg_columnmask` ポリシーを維持します。

## 論理レプリケーション
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.LogRep"></a>

初期同期中、論理レプリケーションは標準の SQL COPY オペレーションを使用し、`pg_columnmask` ポリシーはレプリケーションユーザーのアクセス許可に基づいて適用されます。進行中の CDC (変更データキャプチャ) 中、マスキングポリシーは適用されず、マスクされていないデータは WAL レコードを介してレプリケートされます。`pg_create_subscription` 権限を持つユーザーは、管理するシステムへのレプリケーションを設定することで、マスクされていないデータを流出する可能性があります。

## Blue/Green デプロイ
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.BlueGreen"></a>

スナップショットの復元中、`pg_columnmask` ポリシーは自動的に含まれます。グリーン環境は、ブルー環境のすべてのポリシーの同じコピーから始まります。ブルーからグリーンへのレプリケーション中、データはマスクされません。ブルークラスターでのその後のマスキングポリシーの変更 (DDL コマンド) は、グリーンクラスターにレプリケートされず、RDS ブルー/グリーンデプロイが無効になります。

## ゼロ ETL ストリームと CDC ストリーム
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.ZETL"></a>

データレプリケーションは `pg_columnmask` ポリシーの影響を受けません。ゼロ ETL は DDL レプリケーションをサポートしますが、`pg_columnmask` または RLS ポリシーはレプリケートしません。ゼロ ETL のレプリケートされたデータにはマスキングポリシーは適用されません。

## AWS Database Migration Service
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DMS"></a>

初期データ同期は、DMS タスク用に選択されたユーザーに基づいてマスクまたはマスク解除されます。CDC データは常にマスク解除されます。`pg_columnmask` 関連の内部 RLS ポリシーは移行できますが、pg\$1columnmask 対応以外のターゲットでは機能しません。

## データエクスポート
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DataExport"></a>

`pg_columnmask` はエクスポートを他のクエリオペレーションと同様に扱います。マスキングは実行中のユーザーのアクセス許可に基づいて適用されます。これは、COPY、SELECT INTO、CREATE TABLE AS、Aurora PostgreSQL の S3 エクスポート機能などの SQL コマンドに適用されます。

**注記**  
マスクされたユーザーがデータをエクスポートする場合、結果のファイルには、復元時にデータベースの制約に違反する可能性のあるマスクされた値が含まれます。

## ビューとマテリアライズドビュー
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.Views"></a>

ビューと連携させるときは、次の考慮事項に注意してください。
+ **通常のビュー** – 常に `INVOKER` セマンティクスを使用します。現在のユーザーのマスキングポリシーは、ビューを作成したユーザーに関係なく、ビューのクエリ時に適用されます。
+ **マテリアライズドビュー** – 更新すると、更新を実行するユーザーのポリシーではなく、マテリアライズドビュー所有者のマスキングポリシーが適用されます。所有者にマスキングポリシーがある場合、マテリアライズドビューには常にマスクされたデータが含まれます。

## データダンプと復元
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.DDR"></a>

`pg_dump` は通常のデータベースユーザーとして動作し、接続するユーザーのアクセス許可に基づいてマスキングポリシーを適用します。マスクされたユーザーがダンプを実行する場合、バックアップファイルにはマスクされたデータが含まれます。`pg_columnmask` ポリシーはデータベーススキーマの一部としてダンプに含まれます。復元を成功させるには、参照されるすべてのロールがターゲットデータベースに存在し、ターゲットに `pg_columnmask` 拡張機能がインストールされている必要があります。

**注記**  
PostgreSQL 18 以降、`pg_dump` は、行レベルセキュリティ (RLS) ポリシーと `pg_columnmask` マスキングポリシーの両方をデータベースダンプから除外する `—no-policies` オプションをサポートしています。詳細については、「[pg\$1dump](https://www.postgresql.org/docs/current/app-pgdump.html)」を参照してください。

## 外部データラッパー
<a name="AuroraPostgreSQL.Security.DynamicMasking.DataMovement.FDQ"></a>

外部データラッパーを使用する場合、リモートテーブルのマスキングポリシーは、ローカルクエリユーザーのアクセス許可ではなく、マッピングされたユーザーのソースサーバーに対するアクセス許可に基づいて適用され、FDW を介してマスクされたリモートデータにアクセスできますが、ローカルデータベースの外部テーブルで DDM または RLS ポリシーを直接作成することはできません。