行レベルセキュリティ(Row-Level Security, RLS)は、同じ表でもユーザーや状況に応じて「見える行」を自動的に絞り込む仕組みです。OracleではDBMS_RLS(Virtual Private Database, VPD)を使って実現し、アプリケーションのSQLを書き換えなくても、データベース側でWHERE句を動的に付与できます。ここではDBMS_RLSの基本概念から、アプリケーションコンテキストの準備、ポリシー関数の実装、ポリシー付与、更新系の取り扱い、列マスキング、運用時の注意点までを実務視点で解説します。
基本概念と全体像
RLSは「ポリシー関数が返す述語(WHERE句断片)を対象表のSQLに自動付与する」という仕組みで動作します。誰がアクセスしているかはSYS_CONTEXTなどから取得し、必要な条件を文字列で返せば、SELECTやUPDATE、DELETEの各文に埋め込まれます。導入ステップはアプリケーションコンテキストの準備、ポリシー関数の作成、DBMS_RLS.ADD_POLICYで対象表へ紐づけ、という流れです。
アプリケーションコンテキストを用意する
ユーザーの部門IDやロールなど、フィルタ条件に使う値をセッションごとに保持するため、アプリケーションコンテキストを作成します。安全に設定するために専用パッケージ経由で値を入れる構成が推奨です。
-- コンテキスト名 APP_CTX を作成(設定パッケージはCTX_PKGに限定)
CREATE CONTEXT app_ctx USING ctx_pkg;
-- 値を設定するパッケージ
CREATE OR REPLACE PACKAGE ctx_pkg AS
PROCEDURE set_dept(p_dept_id NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY ctx_pkg AS
PROCEDURE set_dept(p_dept_id NUMBER) IS
BEGIN
DBMS_SESSION.SET_CONTEXT('APP_CTX','DEPT_ID', TO_CHAR(p_dept_id));
END;
END;
/
-- ログイン後などアプリ側で呼び出す
BEGIN
ctx_pkg.set_dept(10);
END;
/
ポリシー関数を実装する
ポリシー関数は「返り値がWHERE句として結合される」点が本質です。空文字やNULLを返すとフィルタなし、’1=2′ を返すと常に不一致、といった使い分けができます。
-- 売上表 SALES を部門単位で絞る例
CREATE OR REPLACE FUNCTION f_sales_rls(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AS
v_dept VARCHAR2(30) := SYS_CONTEXT('APP_CTX','DEPT_ID');
BEGIN
IF v_dept IS NULL THEN
RETURN '1=2'; -- 未設定は見せない
ELSE
RETURN 'dept_id = ' || TO_NUMBER(v_dept);
END IF;
END;
/
表にポリシーを付与する
DBMS_RLS.ADD_POLICYで対象表に関連付けます。どのステートメント種別(SELECT/INSERT/UPDATE/DELETE)に適用するかを指定でき、必要に応じて動的/静的、共有コンテキストなどのオプションも調整します。
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'SALES',
policy_name => 'SALES_RLS_BY_DEPT',
function_schema => USER,
policy_function => 'F_SALES_RLS',
statement_types => 'SELECT,UPDATE,DELETE', -- 必要に応じて指定
update_check => TRUE -- UPDATE時も新旧値でチェック
);
END;
/
これでアプリ側のSQLに手を入れなくても、たとえば「SELECT * FROM sales」は内部的に「… WHERE dept_id = :ctx_dept」のように書き換えられます。
動作確認とデバッグのコツ
コンテキストを部門10に設定してSELECTすると、dept_id=10の行だけが見えるはずです。部門を20に変更して再実行すれば結果が切り替わります。期待通りに絞れない場合は、ポリシー関数が返す文字列をDBMS_OUTPUTでログ出力するか、一時的に同じロジックを通常のSELECTのWHERE句に入れて動作を比較します。関数の戻りが無効なSQL断片だとORA-28113などのエラーになるため、境界ケースは網羅しておきます。
更新・削除時の扱いとUPDATE_CHECK
UPDATEやDELETEにもRLSを適用すると、「触れる行」自体が絞り込まれます。さらにupdate_check=>TRUEにしておくと、UPDATEで新しい値に変えた結果もポリシーで検証されます。たとえばdept_idを書き換えて他部門へ“移動”してしまう抜け道を塞ぐ用途に有効です。INSERTに対しても、値がポリシーに合致しないなら拒否させたい場合はstatement_typesにINSERTを含め、関数側で新規行の値を参照するパターン(必要に応じてSEC_RELEVANT_COLS関連の機能と併用)を検討します。
列マスキング(SEC_RELEVANT_COLS)の併用
「行は見せるが、機微情報の列はマスクする」要件にはSEC_RELEVANT_COLS/…_OPTを使います。完全に非表示にする戦略のほか、NULL化やアプリ側の表示抑制と組み合わせる設計も現実的です。
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'CUSTOMERS',
policy_name => 'MASK_PHONE',
function_schema => USER,
policy_function => 'F_CUSTOMER_RLS',
statement_types => 'SELECT',
sec_relevant_cols => 'PHONE_NUMBER',
sec_relevant_cols_opt=> DBMS_RLS.ALL_ROWS -- 行は返すが列を保護
);
END;
/
パフォーマンスと実装上の注意
ポリシー関数はアクセスのたびに評価されるため、軽量に保つのが鉄則です。表参照や重い計算は避け、アプリケーションコンテキストに乗る値だけで判定できるようにロジックを設計します。複数条件を返す必要がある場合も、インデックス利用を阻害しない形で述語を返すと実行計画が安定します。例えば関数で列値を包む書き方は避け、単純比較やINリストへ落とし込むのが基本です。テストでは実データ量に近い統計を持つ環境でプランを確認し、必要に応じて結合順序やヒントを検討します。
管理操作(一時停止・変更・削除)
検証や緊急対応でRLSを一時停止したい場合はENABLE_POLICY/DISABLE_POLICYを使います。ポリシー名の変更や関数差し替えはDROP_POLICY→ADD_POLICYの再作成が確実です。
BEGIN
DBMS_RLS.DISABLE_POLICY(USER,'SALES','SALES_RLS_BY_DEPT');
-- 検証作業…
DBMS_RLS.ENABLE_POLICY (USER,'SALES','SALES_RLS_BY_DEPT');
-- 不要になったら削除
DBMS_RLS.DROP_POLICY (USER,'SALES','SALES_RLS_BY_DEPT');
END;
/
RLSを回避できる強力なシステム権限(EXEMPT ACCESS POLICY)を持つユーザーはポリシーの影響を受けません。運用ロールの設計時は権限の過剰付与に注意してください。
多テナントやマルチ条件の設計例
部署だけでなくテナントIDと役割を複合で判定する場合は、コンテキストに複数キーを載せ、返す述語を単純なANDの積にします。例として「tenant_id = :ctx_tenant AND (role = ‘ADMIN’ OR owner_id = :ctx_user_id)」のように展開し、分岐を関数内のIFで作らない代わりに述語を組み替えて返すと、プランがぶれにくくなります。
まとめ
DBMS_RLSは「SQLを書き換えずに、行レベルでアクセス制御を掛ける」ための強力な標準機能です。アプリケーションコンテキストで判定材料をセッションに載せ、軽量なポリシー関数で素直な述語を返し、ADD_POLICYで表に紐づける——この型を押さえれば、既存アプリにも低リスクで適用できます。更新系や列マスキングの選択肢、パフォーマンス最適化、権限設計を合わせて整えれば、セキュアで予測可能なデータアクセス基盤をPL/SQLだけで実現できます。