DBMS_RLS は、Oracle DatabaseでVPD(Virtual Private Database)を実装するための標準パッケージです。同じ表に対するSQLでも、ログインユーザー、テナントID、部門ID、権限ロールなどに応じて、見える行をデータベース側で自動的に絞り込めます。
アプリケーション側のすべてのSQLに WHERE tenant_id = ... のような条件を埋め込む方法は、漏れやすく、保守もしづらくなります。DBMS_RLS を使うと、対象表にポリシーを付与し、SQLの解析時にポリシー関数が返した述語を自動的に追加できます。VPD全体の考え方は Oracle VPD(Virtual Private Database)完全ガイド、セッション情報の扱いは SYS_CONTEXT・アプリケーションコンテキスト完全ガイド もあわせて確認すると整理しやすいです。
DBMS_RLSとVPDの基本動作- アプリケーションコンテキストを使った認可情報の受け渡し
- ポリシー関数の作り方と安全な述語の返し方
DBMS_RLS.ADD_POLICYの主要引数UPDATE_CHECK、INSERT、列マスキングの注意点- デバッグ、無効化、削除、運用設計のチェックポイント
DBMS_RLSとは
DBMS_RLS は、Fine-Grained Access Controlを管理するためのパッケージです。Oracle公式ドキュメントでは、VPDを実装するための管理インターフェースとして説明されています。表、ビュー、シノニムにセキュリティポリシーを関連付け、対象SQLが実行されるときにポリシー関数から動的な条件式を取得します。
全体の実装手順
DBMS_RLSによる行レベルセキュリティは、次の順番で組み立てると事故が少なくなります。
コンテキスト設定の基本は DBMS_SESSION.SET_CONTEXTの利用例 でも解説しています。DBMS_RLSはこの仕組みと組み合わせることで、アプリケーションから渡されたログイン情報をSQL制御に使えます。
サンプル要件
ここでは、売上表 SALES を部門単位で分離する例を使います。部門10の利用者は dept_id = 10 の行だけ、部門20の利用者は dept_id = 20 の行だけを参照できる想定です。セッションに部門IDが設定されていない場合は、誤表示を避けるため何も見せない方針にします。
CREATE TABLE sales ( sales_id NUMBER PRIMARY KEY, dept_id NUMBER NOT NULL, customer_id NUMBER NOT NULL, amount NUMBER NOT NULL, note VARCHAR2(200), created_at DATE DEFAULT SYSDATE ); INSERT INTO sales (sales_id, dept_id, customer_id, amount, note) VALUES (1, 10, 1001, 50000, 'dept 10 order'); INSERT INTO sales (sales_id, dept_id, customer_id, amount, note) VALUES (2, 20, 2001, 75000, 'dept 20 order'); COMMIT;
アプリケーションコンテキストを作成する
RLSで使う部門IDは、アプリケーションコンテキストに保持します。CREATE CONTEXT では、どのパッケージから値を設定できるかを指定します。直接好きな値を書き込める構成にするとアクセス制御の意味が薄れるため、専用パッケージ経由に限定するのが基本です。
CREATE OR REPLACE PACKAGE app_ctx_pkg AS
PROCEDURE set_dept_id(p_dept_id IN NUMBER);
PROCEDURE clear_context;
END app_ctx_pkg;
/
CREATE OR REPLACE PACKAGE BODY app_ctx_pkg AS
PROCEDURE set_dept_id(p_dept_id IN NUMBER) IS
BEGIN
IF p_dept_id IS NULL OR p_dept_id < 1 THEN
RAISE_APPLICATION_ERROR(-20001, 'Invalid department id');
END IF;
DBMS_SESSION.SET_CONTEXT(
namespace => 'APP_CTX',
attribute => 'DEPT_ID',
value => TO_CHAR(p_dept_id)
);
END;
PROCEDURE clear_context IS
BEGIN
DBMS_SESSION.CLEAR_CONTEXT('APP_CTX');
END;
END app_ctx_pkg;
/
CREATE CONTEXT app_ctx USING app_ctx_pkg;
CREATE CONTEXT には権限が必要です。開発者スキーマで自由に作れない環境では、DBAまたは管理用スキーマで作成してもらいます。接続プールを使うWebアプリでは、リクエスト開始時に必ずコンテキストを設定し、利用者切り替え時に古い値が残らないようにすることが重要です。
ポリシー関数を作成する
ポリシー関数は、対象表に追加する述語を文字列で返します。関数の引数は object_schema と object_name です。戻り値が 'dept_id = 10' であれば、その条件が対象SQLに自動的に追加されます。
CREATE OR REPLACE FUNCTION f_sales_rls(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AS
v_dept_id_text VARCHAR2(30);
v_dept_id NUMBER;
BEGIN
v_dept_id_text := SYS_CONTEXT('APP_CTX', 'DEPT_ID');
IF v_dept_id_text IS NULL THEN
RETURN '1 = 2';
END IF;
v_dept_id := TO_NUMBER(v_dept_id_text);
RETURN 'dept_id = ' || TO_CHAR(v_dept_id);
EXCEPTION
WHEN VALUE_ERROR THEN
RETURN '1 = 2';
END;
/
- ユーザー入力をそのまま連結しない。数値なら数値変換してから返します。
- 未設定時に全件表示しない。原則として
1 = 2のように閉じる設計にします。 - 重いSQL、外部呼び出し、複雑な分岐を入れすぎない。SQLの解析・実行に影響します。
- ポリシー関数の所有者権限で参照が解決される点を意識します。権限設計は 権限分離とセキュリティパッケージ の考え方と合わせて設計します。
DBMS_RLS.ADD_POLICYで表に付与する
ポリシー関数を作成したら、DBMS_RLS.ADD_POLICY で対象表に関連付けます。まずは参照系の SELECT に適用し、動作確認後に 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,
policy_type => DBMS_RLS.CONTEXT_SENSITIVE,
enable => TRUE
);
END;
/
statement_types を省略すると、INSERTとINDEXを除く種別に適用されます。INSERTも制御対象にしたい場合は、Oracleの仕様上 update_check => TRUE を指定しないとエラーになるケースがあります。また、DBMS_RLS.ADD_POLICY は実行時にコミットを伴うため、通常の業務トランザクション中に気軽に呼ぶものではありません。DDLやリリース手順の一部として扱うのが安全です。
動作確認する
コンテキストを設定してから同じSELECTを実行すると、見える行が変わります。アプリケーション側のSQLは変えずに、データベース側で条件が付与される点がDBMS_RLSの強みです。
BEGIN app_ctx_pkg.set_dept_id(10); END; / SELECT sales_id, dept_id, customer_id, amount FROM sales ORDER BY sales_id; BEGIN app_ctx_pkg.set_dept_id(20); END; / SELECT sales_id, dept_id, customer_id, amount FROM sales ORDER BY sales_id;
期待どおりに絞れない場合は、まず SYS_CONTEXT('APP_CTX','DEPT_ID') の値を確認します。デバッグ時に DBMS_OUTPUT.PUT_LINE を使う場合は、出力の見方を DBMS_OUTPUTの使い方 で確認しておくと詰まりにくいです。コンパイルエラーや警告が出る場合は PL/SQLコンパイル時エラーと警告の対処 も参考になります。
UPDATE_CHECKの考え方
UPDATE_CHECK は、更新後の値がポリシー条件に合うかを確認するための指定です。たとえば部門10の利用者が、自分に見えている行の dept_id を20へ変更できてしまうと、アクセス制御をすり抜けるような状態になります。update_check => TRUE を指定しておくと、更新後の値もポリシーで検査されます。
BEGIN app_ctx_pkg.set_dept_id(10); END; / -- 部門10の利用者が部門20へ移動させようとする例 UPDATE sales SET dept_id = 20 WHERE sales_id = 1;
RLSの設計では「参照できる行」と「更新してよい行」を分けて考える必要があります。参照だけなら問題ない条件でも、更新・削除まで許可すると業務的に危険なケースがあります。更新系を含める場合は、ロール、業務状態、承認フラグなども含めてテストケースを作っておきます。
INSERTを制御する場合
INSERTもRLSで制御できますが、SELECTやUPDATEより設計が難しくなります。新規行はまだ表に存在しないため、何を基準に許可するかを明確にする必要があります。通常は、アプリケーション側で dept_id を現在のコンテキスト値に固定し、DB側では制約・トリガー・RLSの組み合わせで二重に守ります。
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'SALES',
policy_name => 'SALES_RLS_DML',
function_schema => USER,
policy_function => 'F_SALES_RLS',
statement_types => 'SELECT,INSERT,UPDATE,DELETE',
update_check => TRUE,
policy_type => DBMS_RLS.CONTEXT_SENSITIVE
);
END;
/
INSERTを含めるときは、単に statement_types に追加するだけでなく、失敗時のエラー、アプリケーション側の入力制御、バッチ処理でのコンテキスト設定漏れも確認します。バッチユーザーや管理者処理には、一般利用者とは別のポリシーや運用ルートを用意した方が安全です。
列マスキングにSEC_RELEVANT_COLSを使う
DBMS_RLSは行を絞るだけでなく、特定列が参照されたときにポリシーを効かせる用途にも使えます。sec_relevant_cols に機微列を指定し、sec_relevant_cols_opt => DBMS_RLS.ALL_ROWS を使うと、行は返しつつ条件に合わない列値をNULLとして見せる設計ができます。
CREATE OR REPLACE FUNCTION f_customer_mask(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AS
BEGIN
IF SYS_CONTEXT('APP_CTX', 'ROLE_NAME') = 'SUPPORT_MANAGER' THEN
RETURN NULL;
END IF;
RETURN '1 = 2';
END;
/
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'CUSTOMERS',
policy_name => 'MASK_CUSTOMER_PHONE',
function_schema => USER,
policy_function => 'F_CUSTOMER_MASK',
statement_types => 'SELECT',
sec_relevant_cols => 'PHONE_NUMBER',
sec_relevant_cols_opt => DBMS_RLS.ALL_ROWS
);
END;
/
DBMS_RLS.ALL_ROWS はSELECT向けの列マスキングとして考えます。通常の行フィルタとは挙動が異なり、条件に合わない行を消すのではなく、対象列をNULLにします。アプリケーションがNULLを想定していない場合、集計・検索・画面表示で副作用が出るため、導入前に確認が必要です。
ポリシータイプの選び方
policy_type は、ポリシー関数をどのタイミングで評価するかに関係します。毎回変わる条件なら DYNAMIC、アプリケーションコンテキストの変更に応じて変わる条件なら CONTEXT_SENSITIVE を検討します。接続プールを使うWebアプリでは、同じDBセッションを複数利用者で使い回すため、コンテキストの設定・クリアとポリシータイプの組み合わせが重要です。
デバッグと確認に使うビュー
ポリシーが効いているか確認するときは、まず現在のコンテキスト値、対象表のポリシー定義、実行中カーソルに適用された述語を確認します。環境によって参照できるビューは権限に左右されるため、見えない場合はDBAビューの参照権限を確認してください。
-- 現在セッションのコンテキスト値
SELECT SYS_CONTEXT('APP_CTX', 'DEPT_ID') AS dept_id
FROM dual;
-- ユーザーが所有するポリシー定義
SELECT object_name,
policy_name,
policy_function,
sel,
ins,
upd,
del,
enable
FROM user_policies
WHERE object_name = 'SALES';
-- 実行中カーソルに適用されたVPD述語
SELECT sql_id,
object_owner,
object_name,
policy,
predicate
FROM v$vpd_policy
WHERE object_name = 'SALES';
ポリシー関数が無効なSQL断片を返すと、実行時に ORA-28113 などのエラーになることがあります。本番導入前には、コンテキスト未設定、想定外ロール、NULL値、数値変換エラー、管理者ユーザー、バッチユーザーを含めてテストします。
一時停止・再有効化・削除
検証や障害対応でポリシーを一時停止する場合は、ENABLE_POLICY を使います。関数名や対象条件を大きく変える場合は、変更手順を明確にし、再作成後に依存オブジェクトの状態を確認します。
BEGIN
DBMS_RLS.ENABLE_POLICY(
object_schema => USER,
object_name => 'SALES',
policy_name => 'SALES_RLS_BY_DEPT',
enable => FALSE
);
DBMS_RLS.ENABLE_POLICY(
object_schema => USER,
object_name => 'SALES',
policy_name => 'SALES_RLS_BY_DEPT',
enable => TRUE
);
END;
/
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => USER,
object_name => 'SALES',
policy_name => 'SALES_RLS_BY_DEPT'
);
END;
/
ポリシーの有効・無効や追加・削除はデータの見え方を大きく変えます。本番では作業前後のSQL、対象スキーマ、対象表、確認ユーザー、ロールバック手順を残しておくと安心です。
実務での設計チェックポイント
EXEMPT ACCESS POLICY を持つユーザーはポリシーの影響を受けません。付与先を厳しく管理します。よくある失敗
ポリシー関数で全件表示を返してしまう
コンテキストが未設定のときに NULL を返すと、制限なしとして扱われます。ログイン直後、バッチ起動直後、接続プール再利用直後など、未設定が起こり得る場面では 1 = 2 で閉じる方が安全です。
アプリケーション側のユーザーIDを信用しすぎる
画面から渡された部門IDやユーザーIDをそのままコンテキストに入れると、改ざん時に危険です。ログイン済みユーザーからサーバー側で所属や権限を引き直し、検証済みの値だけを設定します。
管理者・保守ユーザーの動作を確認していない
一般ユーザーでは正しくても、管理者、監査担当、夜間バッチ、データ移行ユーザーでは要件が違うことがあります。RLSを回避できる権限の扱いも含め、運用ロールごとの期待結果を先に決めておきます。
まとめ
DBMS_RLS を使うと、Oracle Database側でVPDによる行レベルセキュリティを実装できます。アプリケーションコンテキストに認可情報を入れ、ポリシー関数で安全な述語を返し、DBMS_RLS.ADD_POLICY で対象表へ付与するのが基本形です。
実務では、未設定時に閉じる設計、UPDATE_CHECK の有効化、INSERT時の扱い、列マスキングのNULL副作用、接続プールでのコンテキストクリア、管理者権限の分離が重要です。DBMS_RLSは強力な機能ですが、SQLの見え方をデータベース層で変えるため、テスト観点と運用手順まで含めて導入すると安定します。

