第7章 ガバナンスとセキュリティ / 想定学習時間:30〜40分 / 最終確認:2026年8月

7-3. 列レベルマスキングと行レベルセキュリティ

🎯 この節の学習目標

1. なぜ「列」と「行」の制御が必要か

7-2 で学んだ GRANT / REVOKE は「テーブル全体を読めるか読めないか」というオブジェクト単位の制御でした。しかし実務の要件はもっと細かいことがよくあります。

Unity Catalog では、この2つをカラムマスク(column mask)行フィルタ(row filter)としてテーブル本体に直接定義できます。どちらも仕組みは共通で、「判定ロジックを SQL UDF(ユーザー定義関数)として作成し、ALTER TABLE でテーブルに取り付ける」という2段構えです。判定によく使うのが is_account_group_member('グループ名') という組み込み関数で、クエリを実行している本人が指定グループのメンバーなら true を返します。

2. カラムマスク:非許可ユーザーには伏字を返す

カラムマスクは、列の値を返す直前に UDF を通す仕組みです。UDF は「マスク対象の列の値」を引数に受け取り、「実際に返す値」を返します。許可されたユーザーには元の値を、それ以外には伏字を返すように書きます。

💡 具体例:メールアドレス列をマスクする

-- 手順1:マスキング関数(SQL UDF)を作成する
--   引数の型はマスク対象列と互換であること
CREATE OR REPLACE FUNCTION main.security.mask_email(email STRING)
RETURNS STRING
RETURN CASE
  WHEN is_account_group_member('pii_readers') THEN email  -- 許可グループには生値
  ELSE '***MASKED***'                                     -- それ以外は伏字
END;

-- 手順2:テーブルの列にマスクを適用する
ALTER TABLE main.crm.customers
  ALTER COLUMN email SET MASK main.security.mask_email;

-- 解除する場合
ALTER TABLE main.crm.customers
  ALTER COLUMN email DROP MASK;

適用後は、pii_readers グループのメンバーが SELECT email FROM main.crm.customers を実行すると生のメールアドレスが、それ以外のユーザーには ***MASKED*** が返ります。クエリを書き換える必要はなく、同じテーブルへの同じクエリで、実行者によって結果だけが変わるのがポイントです。

伏字の代わりに「一部だけ見せる」加工も UDF 次第で自由に書けます。たとえば CONCAT('***@', split(email, '@')[1]) のようにドメイン部分だけ残す、といった部分マスクも実装できます。

3. 行フィルタ:見える行そのものを絞る

行フィルタは、クエリ結果に含めるかどうかを行ごとに判定する仕組みです。UDF は「フィルタ条件に使う列の値」を引数に受け取り、BOOLEAN を返します。true の行だけが結果に含まれます。

💡 具体例:担当地域の行だけを見せる

-- 手順1:フィルタ関数を作成する(BOOLEAN を返す)
CREATE OR REPLACE FUNCTION main.security.region_filter(region STRING)
RETURNS BOOLEAN
RETURN
  is_account_group_member('sales_admins')                       -- 管理者は全行
  OR (is_account_group_member('sales_east') AND region = 'east') -- 東日本担当は east のみ
  OR (is_account_group_member('sales_west') AND region = 'west'); -- 西日本担当は west のみ

-- 手順2:テーブルに行フィルタを適用する
--   ON (列名) で、テーブルのどの列を UDF の引数に渡すかを指定する
ALTER TABLE main.sales.orders
  SET ROW FILTER main.security.region_filter ON (region);

-- 解除する場合
ALTER TABLE main.sales.orders DROP ROW FILTER;

適用後、sales_east のメンバーが SELECT count(*) FROM main.sales.orders を実行すると、east の行だけを数えた結果が返ります。フィルタされた行は存在自体が見えなくなるため、集計結果も自動的に「本人が見てよい範囲」のものになります。

📝 試験のポイント

構文の対応関係を混同しないようにしましょう。列を隠す=カラムマスク=ALTER COLUMN ... SET MASK(UDF は列と同じ型の値を返す)行を絞る=行フィルタ=SET ROW FILTER ... ON (列)(UDF は BOOLEAN を返す)です。また、どちらも「テーブルを SELECT できること」が前提です。マスクやフィルタは 7-2 の GRANT を置き換えるものではなく、SELECT 権限がある人の中で見え方を変える仕組みだと整理してください。

4. ビューによる制御との比較

カラムマスクや行フィルタが登場する前からある古典的な方法が、ビューによる制御です。元テーブルへの直接アクセスは与えず、CASE WHEN is_account_group_member(...) や WHERE 句を仕込んだビューだけを公開するやり方です。

-- ビュー方式の例:マスクとフィルタをビューに埋め込む
CREATE VIEW main.crm.customers_safe AS
SELECT
  customer_id,
  CASE WHEN is_account_group_member('pii_readers')
       THEN email ELSE '***MASKED***' END AS email,
  region
FROM main.crm.customers
WHERE is_account_group_member('sales_admins') OR region = 'east';
観点ビューによる制御カラムマスク / 行フィルタ
適用対象ビューという別オブジェクトを公開(元テーブルは隠す)テーブル本体に直接適用
利用者から見た名前用途別にビュー名が増える(_safe_east…)全員が同じテーブル名を使い、結果だけ変わる
管理負荷要件が増えるほどビューが乱立し、定義の重複・更新漏れが起きやすいポリシーは UDF に集約。テーブルと1対1で管理できる
直接アクセス元テーブルの SELECT を渡すと制御を素通りされるテーブル自体に効くため、直接 SELECT しても制御される

ビュー方式は手軽で柔軟ですが、部署×地域×役職のように条件が組み合わさると「同じテーブルの派生ビュー」が乱立し、どのビューが最新のセキュリティ要件を反映しているのか分からなくなりがちです。テーブル本体に1つだけ真実のポリシーを取り付けられるのがマスク/フィルタ方式の利点です。

5. PII 保護のユースケースと動作確認

典型的なユースケースは PII(個人識別情報)の保護です。氏名・メールアドレス・電話番号・住所などの列にカラムマスクを適用し、コンプライアンス上許可されたグループだけが生値を見られるようにします。行フィルタは、地域・部門・契約単位でのデータ分離(マルチテナント的な見せ方)によく使われます。1つのテーブルに両方を同時に適用することもできます。

💡 具体例:動作確認の方法

適用後は必ず許可グループのメンバーと非メンバーの両方の立場で同じクエリを実行し、結果が意図どおり変わることを確認します。自分がどのグループに属しているかは SELECT is_account_group_member('pii_readers'); で確認できます(true/false が返ります)。テーブルにどのマスク・フィルタが付いているかは、DESCRIBE EXTENDED や Catalog Explorer のテーブル詳細画面で確認できます。「マスクを適用したつもりが、検証を許可グループのアカウントだけで行ったため、伏字を一度も確認していなかった」という失敗は起こりがちなので、非許可側での確認を必ず含めてください。

✅ この節のまとめ

練習問題

問1. 顧客テーブルの phone 列を、support_leads グループ以外には伏字で見せたい。正しい実装手順はどれか。

  1. BOOLEAN を返す UDF を作成し、ALTER TABLE ... SET ROW FILTER で適用する
  2. is_account_group_member('support_leads') で分岐して列値または伏字を返す UDF を作成し、ALTER TABLE ... ALTER COLUMN phone SET MASK で適用する
  3. DENY SELECT を phone 列に対して発行する
  4. GRANT SELECT をテーブルから REVOKE すれば列も見えなくなる
解答と解説を見る

正解:B

列単位で値を伏字化する要件はカラムマスクです。UDF で許可グループには生値、それ以外には伏字を返し、SET MASK で列に取り付けます。Aは行フィルタの手順で、行の絞り込み用です。Cは誤りで、そもそも Unity Catalog に DENY はなく(レガシーの hive_metastore のみ)、あったとしても列単位の SELECT の制御はできません。Dはテーブル全体が読めなくなり、「他の列は見せたい」という要件を満たしません。

問2. 行フィルタ用の SQL UDF が満たすべき条件として正しいものはどれか。

  1. 戻り値は BOOLEAN で、true を返した行だけがクエリ結果に含まれる
  2. 戻り値はフィルタ対象列と同じ型で、返した値が行の値として表示される
  3. 戻り値は必ず STRING で、'ALLOW' または 'DENY' を返す
  4. UDF は使えず、フィルタ条件はテーブルプロパティに文字列で記述する
解答と解説を見る

正解:A

行フィルタの UDF は BOOLEAN を返し、true の行のみが結果に含まれます。Bはカラムマスク用 UDF の説明で、行フィルタとは戻り値の役割が異なります。Cのような文字列プロトコルはありません。Dも誤りで、行フィルタはまさに SQL UDF を SET ROW FILTER で取り付ける仕組みです。

問3. これまで部署ごとに数十個のセキュアビューを作って行・列の制御をしてきたが、定義の重複と更新漏れが問題になっている。カラムマスク/行フィルタ方式へ移行する利点として最も適切なものはどれか。

  1. ビューと違い、マスクやフィルタを適用したテーブルはクエリ性能が必ず向上する
  2. 制御をテーブル本体に直接適用できるため、利用者は同じテーブル名を使え、部署別ビューの乱立と定義の重複を解消できる
  3. マスクを適用すればテーブルへの SELECT 権限の付与が不要になる
  4. 行フィルタを使うと、元テーブルに直接クエリした場合のみ全行が見える安全な抜け道を用意できる
解答と解説を見る

正解:B

マスク/フィルタはテーブル本体に取り付けるため、用途別ビューを量産する必要がなくなり、ポリシーの一元管理ができます。Aは誤りで、性能向上を保証する機能ではありません(判定処理はむしろ追加コストです)。Cも誤りで、マスクは SELECT 権限を前提とした「見え方の制御」であり、権限付与の代替ではありません。Dは逆で、テーブル本体に効くからこそ直接クエリでも制御を素通りできない点が利点です。

問4. 次の SQL を実行した。
ALTER TABLE main.sales.orders SET ROW FILTER main.security.region_filter ON (region);
この後、sales_east グループ(east 地域のみ許可)のメンバーが SELECT count(*) FROM main.sales.orders を実行した場合の結果はどれか。

  1. テーブルの全行数が返る。集計クエリには行フィルタが適用されないため
  2. region = 'east' の行だけを数えた件数が返る
  3. 権限エラーになる。行フィルタが適用されたテーブルは管理者しかクエリできないため
  4. すべての行が 0 件として返る。フィルタ対象の行は NULL に置換されるため
解答と解説を見る

正解:B

行フィルタは結果に含める行そのものを絞るため、count(*) などの集計にも「本人が見てよい行」だけが反映されます。Aは誤りで、集計クエリだけ素通りできる仕様ならセキュリティとして成立しません。Cも誤りで、SELECT 権限があれば通常どおりクエリできます。Dの「NULL に置換」はカラムマスク的な発想で、行フィルタでは行が結果から除外されます。