【PL/SQL】DBMS_SESSION.SET_CONTEXTの使い方|SYS_CONTEXT・VPD・接続プール対応まで

【PL/SQL】セキュリティコンテキスト(DBMS_SESSION.SET_CONTEXT)の利用例 PL/SQL

DBMS_SESSION.SET_CONTEXT は、Oracleのアプリケーションコンテキストへ値を設定するための手続きです。テナントID、ログインユーザーID、部門ID、ロール、リクエストIDなどをセッションに保持し、SYS_CONTEXT でSQLやPL/SQLから参照できます。

この記事では、アプリケーションコンテキスト全体の仕様解説ではなく、PL/SQLで SET_CONTEXT を安全に設定・参照・クリアする実装テンプレートに絞って整理します。体系的な仕様は SYS_CONTEXT・アプリケーションコンテキスト完全ガイド、VPD全体の考え方は VPD(Virtual Private Database)完全ガイド もあわせて確認してください。

この記事で扱うこと

  • CREATE CONTEXT ... USING と信頼済みパッケージ
  • DBMS_SESSION.SET_CONTEXT / SYS_CONTEXT の基本
  • 接続プールでの設定・クリアテンプレート
  • CLEAR_CONTEXTCLEAR_ALL_CONTEXTLIST_CONTEXT
  • SET_IDENTIFIER と監査ログへの活用
  • VPD / DBMS_RLS との連携
スポンサーリンク

アプリケーションコンテキストとは

アプリケーションコンテキストは、セッションに紐づく名前付き属性の入れ物です。名前空間を作成し、その中に TENANT_IDAPP_USER_IDROLE_NAME のような属性を設定します。設定した値は、SQLやPL/SQLから SYS_CONTEXT(namespace, attribute) で参照できます。

namespaceアプリケーション単位の名前空間です。例: APP_CTX
attribute名前空間内のキーです。例: TENANT_ID
value格納する値です。SET_CONTEXT の値はVARCHAR2として扱います。
参照SYS_CONTEXT でSQLやPL/SQLから参照します。

CREATE CONTEXTで名前空間を作る

最初に、コンテキスト名前空間を作成します。重要なのは、USING 句で指定したパッケージが、そのコンテキストを設定できる信頼済みパッケージになる点です。Oracle公式でも、SET_CONTEXT はこの指定パッケージから直接または間接的に呼ばれる必要があると説明されています。

create-context.sql
-- DBAまたはCREATE ANY CONTEXT権限を持つユーザーで実行
CREATE OR REPLACE CONTEXT app_ctx USING app_ctx_pkg;

-- 作成後、APP_CTXの値は app_ctx_pkg 経由で設定する

CREATE CONTEXT 実行時点では、指定したパッケージが存在するかまでは厳密に検証されません。ただし実運用では、先に仕様を決め、パッケージ名・所有スキーマ・権限を明確にしてから作成する方が安全です。

信頼済みパッケージでSET_CONTEXTする

DBMS_SESSION.SET_CONTEXT は、任意のPL/SQLブロックから自由に呼べるものではありません。CREATE CONTEXT app_ctx USING app_ctx_pkg と定義したなら、app_ctx_pkg のコールスタック内から設定します。この制約により、利用者が好きなテナントIDやロールを勝手に設定する事故を防ぎます。

trusted-package.sql
CREATE OR REPLACE PACKAGE app_ctx_pkg AUTHID DEFINER AS
  PROCEDURE set_login_context(
    p_app_user_id IN NUMBER,
    p_tenant_id   IN NUMBER,
    p_role_name   IN VARCHAR2
  );

  PROCEDURE clear_login_context;
END app_ctx_pkg;
/

CREATE OR REPLACE PACKAGE BODY app_ctx_pkg AS
  PROCEDURE set_login_context(
    p_app_user_id IN NUMBER,
    p_tenant_id   IN NUMBER,
    p_role_name   IN VARCHAR2
  ) IS
  BEGIN
    IF p_app_user_id IS NULL OR p_tenant_id IS NULL THEN
      RAISE_APPLICATION_ERROR(-20001, 'login context is required');
    END IF;

    DBMS_SESSION.SET_CONTEXT('APP_CTX', 'APP_USER_ID', TO_CHAR(p_app_user_id));
    DBMS_SESSION.SET_CONTEXT('APP_CTX', 'TENANT_ID',   TO_CHAR(p_tenant_id));
    DBMS_SESSION.SET_CONTEXT('APP_CTX', 'ROLE_NAME',   UPPER(p_role_name));
  END;

  PROCEDURE clear_login_context IS
  BEGIN
    DBMS_SESSION.CLEAR_ALL_CONTEXT('APP_CTX');
  END;
END app_ctx_pkg;
/

パッケージは AUTHID DEFINER にし、必要な検証をパッケージ内に閉じ込めます。権限境界の考え方は 権限分離とセキュリティパッケージ、AUTHID設計は AUTHIDと権限管理の設計 も参考になります。

SYS_CONTEXTで値を参照する

設定した値は SYS_CONTEXT で参照します。PL/SQLの分岐、SQLのWHERE句、監査ログ、VPDポリシー関数などで使えます。

read-context.sql
SELECT SYS_CONTEXT('APP_CTX', 'APP_USER_ID') AS app_user_id,
       SYS_CONTEXT('APP_CTX', 'TENANT_ID')   AS tenant_id,
       SYS_CONTEXT('APP_CTX', 'ROLE_NAME')   AS role_name
FROM dual;

CREATE OR REPLACE FUNCTION current_tenant_id
RETURN NUMBER
IS
BEGIN
  RETURN TO_NUMBER(SYS_CONTEXT('APP_CTX', 'TENANT_ID'));
EXCEPTION
  WHEN VALUE_ERROR THEN
    RETURN NULL;
END;
/

SYS_CONTEXT の戻り値は文字列です。数値や日付として使う場合は、変換エラー時の扱いを決めておきます。未設定時にNULLになることも考慮し、アクセス制御では未設定を許可側に倒さない設計が重要です。

接続プールでの設定・クリア

Webアプリケーションでは、DBセッションが接続プールで使い回されます。前の利用者のコンテキストが残ると重大な情報漏えいになります。そのため、リクエスト開始時に必ず設定し、終了時または返却前に必ずクリアします。

connection-pool-template.sql
-- リクエスト開始時
BEGIN
  app_ctx_pkg.clear_login_context;

  app_ctx_pkg.set_login_context(
    p_app_user_id => :app_user_id,
    p_tenant_id   => :tenant_id,
    p_role_name   => :role_name
  );

  DBMS_SESSION.SET_IDENTIFIER(:request_id);
END;
/

-- リクエスト終了時、または接続をプールへ返す直前
BEGIN
  app_ctx_pkg.clear_login_context;
  DBMS_SESSION.CLEAR_IDENTIFIER;
END;
/

DBMS_SESSION.SET_IDENTIFIERCLIENT_IDENTIFIER を設定するために使います。アプリ利用者ID、リクエストID、ジョブIDなどを入れておくと、監査ログやセッション確認がしやすくなります。監査ログ設計は ロギングとトレーサビリティを両立する監査設計 と組み合わせると実務的です。

CLEAR_CONTEXTとCLEAR_ALL_CONTEXT

特定属性だけ消したい場合は CLEAR_CONTEXT、名前空間全体を消したい場合は CLEAR_ALL_CONTEXT を使います。これらも信頼済みパッケージから直接または間接的に呼ぶ必要があります。

clear-context.sql
-- app_ctx_pkg パッケージボディ内の実装例
PROCEDURE clear_tenant IS
BEGIN
  DBMS_SESSION.CLEAR_CONTEXT('APP_CTX', NULL, 'TENANT_ID');
END;

PROCEDURE clear_login_context IS
BEGIN
  DBMS_SESSION.CLEAR_ALL_CONTEXT('APP_CTX');
END;

接続プールでは、特定属性だけ消すより、リクエスト境界で名前空間全体をクリアする方が安全です。属性を追加したときにクリア漏れが起きにくくなります。

LIST_CONTEXTで現在値を確認する

開発時には DBMS_SESSION.LIST_CONTEXT で現在セッションのコンテキストを確認できます。本番では機微情報が含まれる可能性があるため、出力先やログ化には注意してください。

list-context.sql
SET SERVEROUTPUT ON
DECLARE
  l_contexts DBMS_SESSION.APPCTXTABTYP;
  l_count    NUMBER;
BEGIN
  DBMS_SESSION.LIST_CONTEXT(l_contexts, l_count);

  FOR i IN 1 .. l_count LOOP
    DBMS_OUTPUT.PUT_LINE(
      l_contexts(i).namespace || '.' ||
      l_contexts(i).attribute || '=' ||
      l_contexts(i).value
    );
  END LOOP;
END;
/

デバッグ出力の見方は DBMS_OUTPUTの使い方 も参考になります。

VPDとDBMS_RLSで使う

アプリケーションコンテキストは、VPDのポリシー関数でよく使われます。たとえば TENANT_ID をセッションに設定し、ポリシー関数が tenant_id = ... の条件を返すことで、SQLを書き換えずにテナント分離できます。

vpd-policy-function.sql
CREATE OR REPLACE FUNCTION f_order_rls(
  p_schema IN VARCHAR2,
  p_object IN VARCHAR2
) RETURN VARCHAR2
AS
  l_tenant_id NUMBER;
BEGIN
  l_tenant_id := TO_NUMBER(SYS_CONTEXT('APP_CTX', 'TENANT_ID'));

  RETURN 'tenant_id = ' || TO_CHAR(l_tenant_id);
EXCEPTION
  WHEN VALUE_ERROR THEN
    RETURN '1 = 2';
END;
/

BEGIN
  DBMS_RLS.ADD_POLICY(
    object_schema   => USER,
    object_name     => 'ORDERS',
    policy_name     => 'ORDERS_BY_TENANT',
    function_schema => USER,
    policy_function => 'F_ORDER_RLS',
    statement_types => 'SELECT,UPDATE,DELETE',
    update_check    => TRUE
  );
END;
/

VPDの実装は DBMS_RLSの使い方 でも詳しく扱っています。未設定時に 1 = 2 を返して閉じる設計にしておくと、コンテキスト設定漏れによる全件表示を避けやすくなります。

ACCESSED GLOBALLYとclient_id

通常のセッションローカルコンテキストでは、そのセッション内だけで値を参照します。一方、ACCESSED GLOBALLY を指定したグローバルアプリケーションコンテキストでは、SET_CONTEXTusername / client_id パラメータを使って値を管理できます。

global-context.sql
CREATE OR REPLACE CONTEXT app_global_ctx
USING app_global_ctx_pkg
ACCESSED GLOBALLY;

CREATE OR REPLACE PACKAGE BODY app_global_ctx_pkg AS
  PROCEDURE set_global_value(
    p_client_id IN VARCHAR2,
    p_tenant_id IN NUMBER
  ) IS
  BEGIN
    DBMS_SESSION.SET_CONTEXT(
      namespace => 'APP_GLOBAL_CTX',
      attribute => 'TENANT_ID',
      value     => TO_CHAR(p_tenant_id),
      username  => SYS_CONTEXT('USERENV', 'SESSION_USER'),
      client_id => p_client_id
    );
  END;
END;
/

グローバルコンテキストは便利ですが、通常のWebアプリで安易に使うと追跡やクリアが難しくなります。まずはセッションローカルコンテキストで要件を満たせるか確認し、必要な場合だけ採用します。

よくある失敗

SET_CONTEXTを無名ブロックから直接呼んで失敗する

CREATE CONTEXT ... USING で指定したパッケージのコールスタック内から呼ばれていない可能性があります。必ず専用パッケージを通してください。

接続プールで前の利用者の値が残る

リクエスト開始時に上書きしているつもりでも、新しい属性を追加したときに漏れることがあります。リクエスト開始時と終了時に CLEAR_ALL_CONTEXT する方が安全です。

コンテキスト値を信用しすぎる

画面から渡されたテナントIDやロールをそのまま設定すると危険です。ログイン済みユーザーからサーバー側で所属や権限を引き直し、検証済みの値だけを設定します。

RESULT_CACHE関数でSYS_CONTEXTを読む

セッション依存値を読む関数に RESULT_CACHE を付けると、利用者ごとに結果が違う前提と衝突しやすくなります。関数キャッシュの注意点は FUNCTION RESULT_CACHEの使い方 も確認してください。

本番前チェックリスト

名前空間CREATE CONTEXT ... USING のパッケージ名が正しいか。
信頼済みパッケージSET_CONTEXT / CLEAR_CONTEXT を専用パッケージ経由にしているか。
入力検証ユーザー入力をそのままコンテキスト値にしていないか。
接続プールリクエスト開始・終了でクリアしているか。
未設定時VPDや判定関数で未設定を許可側に倒していないか。
監査SET_IDENTIFIER やログで利用者・リクエストを追跡できるか。
機微情報コンテキストにパスワードやトークンを入れていないか。
テスト利用者切り替え、テナント切り替え、未設定、異常値をテストしたか。

まとめ

DBMS_SESSION.SET_CONTEXT を使うと、PL/SQLやSQLから参照できるアプリケーションコンテキストへ、ログインユーザーやテナントIDなどの値を設定できます。ただし、重要なのは CREATE CONTEXT ... USING で指定した信頼済みパッケージを通して設定・クリアすることです。

接続プール環境では、リクエストごとに設定し、返却前に必ずクリアします。VPDや監査ログと組み合わせると強力ですが、未設定時の扱い、入力検証、機微情報の保存禁止、セッション依存関数との組み合わせまで含めて設計しましょう。