【PL/SQL】マルチスキーマ環境での権限管理とInvoker Rights設計

【PL/SQL】マルチスキーマ環境での権限管理とInvoker Rights設計 PL/SQL

マルチスキーマでPL/SQLを運用する場合、データを保持するスキーマとアプリケーションAPIを提供するスキーマ、さらにはバッチや外部接続用の技術スキーマが併存し、権限の境界が曖昧になると保守性とセキュリティの両面で脆弱になる。Invoker Rights(AUTHID CURRENT_USER)とDefiner Rights(AUTHID DEFINER)の特性を理解し、どの層がどの権限で実行されるべきかを体系的に分解した上で、最小権限と監査可能性を損なわないAPI境界を設計することが重要である。本稿では、スキーマ分離を前提にした権限モデル、Invoker/Definerの使い分け、ロールと直接権限の扱い、シノニムやビューを用いた境界設計、検証と運用に耐えるテスト手順までを実務の観点から解説する。

権限モデルの前提と分離戦略

データの所有者であるスキーマをDATA、アプリケーションAPIを公開するスキーマをAPPとし、外部接続ユーザーをAPP_USERとして切り分ける。DATAはテーブルやインデックスなどの永続オブジェクトを所有し、APPはPL/SQLパッケージを所有する。APPがDATAにアクセスする方式は二つあり、一つはAPPのDefiner RightsでDATAへの権限を直接保持して操作する方式、もう一つはAPPのInvoker Rightsで呼び出し元の権限を借用してDATAへアクセスさせる方式である。機密操作や一貫性の確保が必要な更新はDefinerで封じ、利用者の可視性に依存する参照やテナント分離の判定はInvokerで成立させるのが原則となる。

Definer RightsとInvoker Rightsの動作差分

Definer Rightsはパッケージの所有者であるAPPの権限で実行され、ロールは実行時に無効化されるため、APPにはDATAへの必要最小限のオブジェクト権限を直接付与する必要がある。Invoker Rightsは呼び出し元であるAPP_USERの権限で実行され、名前解決と権限チェックはAPP_USER側のスキーマコンテキストで行われる。Invokerでは一般にロールの効果も反映されるため、アプリ層のアクセス制御をデータベースロールで柔軟に切り替える設計が可能になる。これらの性質から、更新系の内部一括処理や権限昇格が必要なメタデータ操作はDefinerで閉じ、テナントや部署単位で可視性が異なる参照系はInvokerで実行させる使い分けが実務に適合する。

スキーマと権限の初期設定例

アカウント作成と基本権限の配備は、DATAにはAPPに対するオブジェクト権限の供与、APPにはパッケージ作成権限、APP_USERにはAPPパッケージの実行権限とDATA参照権限(Invokerを活かす場合)を与える。シノニムを用いてAPPからDATAオブジェクトを参照する名前解決も統一する。


-- スキーマ作成(パスワードなどは簡略化)
CREATE USER data IDENTIFIED BY "******" QUOTA UNLIMITED ON users;
CREATE USER app  IDENTIFIED BY "******" QUOTA UNLIMITED ON users;
CREATE USER app_user IDENTIFIED BY "******";

GRANT CREATE SESSION TO data, app, app_user;
GRANT CREATE TABLE, CREATE PROCEDURE TO data;
GRANT CREATE PROCEDURE TO app;

-- サンプルテーブル
CONN data/******
CREATE TABLE orders (
  order_id    NUMBER PRIMARY KEY,
  customer_id NUMBER NOT NULL,
  amount      NUMBER(12,2) NOT NULL,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);

-- APPに対しDATAオブジェクトへの最小権限を付与
GRANT SELECT, INSERT, UPDATE ON data.orders TO app;

-- APP_USERにはInvoker運用を想定して参照ロールを付与(例)
CREATE ROLE r_read_orders;
GRANT SELECT ON data.orders TO r_read_orders;
GRANT r_read_orders TO app_user;

-- APPからの名前解決を簡素化するためのシノニム
CONN app/******
CREATE SYNONYM orders FOR data.orders;

APIの境界設計とAUTHIDの使い分け

同一のドメインでも更新系はDefinerで閉じ、参照系はInvokerで可視性を委譲する。更新系パッケージはAPPの直接権限で動作し、参照系はAPP_USERのロールや行レベルセキュリティの効果を受けるように設計する。


-- 参照系:Invoker Rights(呼び出し元の権限・ロールを反映)
CREATE OR REPLACE PACKAGE pkg_order_read AUTHID CURRENT_USER AS
  FUNCTION get_amount(p_order_id IN NUMBER) RETURN NUMBER;
END pkg_order_read;
/
CREATE OR REPLACE PACKAGE BODY pkg_order_read AS
  FUNCTION get_amount(p_order_id IN NUMBER) RETURN NUMBER IS
    v_amount NUMBER;
  BEGIN
    SELECT amount INTO v_amount FROM orders WHERE order_id = p_order_id;
    RETURN v_amount;
  END;
END pkg_order_read;
/

-- 更新系:Definer Rights(APPの直接権限のみで動作)
CREATE OR REPLACE PACKAGE pkg_order_write AUTHID DEFINER AS
  PROCEDURE upsert_order(p_order_id IN NUMBER, p_customer_id IN NUMBER, p_amount IN NUMBER);
END pkg_order_write;
/
CREATE OR REPLACE PACKAGE BODY pkg_order_write AS
  PROCEDURE upsert_order(p_order_id IN NUMBER, p_customer_id IN NUMBER, p_amount IN NUMBER) IS
  BEGIN
    MERGE INTO orders d
    USING (SELECT p_order_id id, p_customer_id cid, p_amount amt FROM dual) s
    ON (d.order_id = s.id)
    WHEN MATCHED THEN UPDATE SET d.customer_id = s.cid, d.amount = s.amt
    WHEN NOT MATCHED THEN INSERT (order_id, customer_id, amount) VALUES (s.id, s.cid, s.amt);
  END;
END pkg_order_write;
/
-- 実行権限の付与
GRANT EXECUTE ON pkg_order_read  TO app_user;
GRANT EXECUTE ON pkg_order_write TO app_user;

Invokerにおける名前解決と権限失効の落とし穴

Invokerでは参照名が呼び出し元のスキーマで解決されるため、APP側に存在しないシノニム名をAPP_USER側でも解決できるようにする必要がある。APPにのみ存在するシノニムをInvokerユニットの中で使用すると、呼び出し元で解決できず実行時エラーとなる。これを避けるには、完全修飾名でDATAスキーマを明示するか、PUBLIC SYNONYMとして定義しておく。完全修飾は依存が明確になる一方、移送性が下がりがちであり、PUBLIC SYNONYMは命名衝突の監視が必要であるため、運用ポリシに応じて選択する。


-- 完全修飾でInvokerの名前解決を安定化
CREATE OR REPLACE PACKAGE BODY pkg_order_read AUTHID CURRENT_USER AS
  FUNCTION get_amount(p_order_id IN NUMBER) RETURN NUMBER IS
    v_amount NUMBER;
  BEGIN
    SELECT amount INTO v_amount FROM data.orders WHERE order_id = p_order_id;
    RETURN v_amount;
  END;
END pkg_order_read;
/

権限昇格が必要な内部処理を安全に封じる

監査や整合性上の理由で一般利用者に許可できない操作は、Definerパッケージ内部に限定し、外部には必要最小限のAPIとして露出させる。セッションコンテキストを付与して監査可能性を担保し、例外時には統一ロガーに記録してから再送出する。


CREATE OR REPLACE PACKAGE pkg_admin_maint AUTHID DEFINER AS
  PROCEDURE reassign_customer(p_from IN NUMBER, p_to IN NUMBER);
END pkg_admin_maint;
/
CREATE OR REPLACE PACKAGE BODY pkg_admin_maint AS
  PROCEDURE reassign_customer(p_from IN NUMBER, p_to IN NUMBER) IS
  BEGIN
    UPDATE data.orders SET customer_id = p_to WHERE customer_id = p_from;
  EXCEPTION
    WHEN OTHERS THEN
      -- ロギングの呼び出し例(実装は別パッケージ)
      -- pkg_logger.log_error(SQLERRM, '{ "from":'||p_from||',"to":'||p_to||'}');
      RAISE;
  END;
END pkg_admin_maint;
/
-- 実行権限の付与は限定ユーザーやロールのみに絞る
GRANT EXECUTE ON pkg_admin_maint TO r_ops_admin;

ロールと直接権限の使い分けとテスト手順

Definerユニットは実行時にロールが無効であるため、必要なオブジェクト権限はAPPへ直接付与しておく。Invokerユニットは呼び出し元のロール構成が反映されるため、ロールで可視性を切り替えられる。テストでは二名以上のユーザーを用意し、同一のInvokerパッケージを実行して結果の差分を検証する。


-- テストユーザーの作成
CREATE USER u_sales IDENTIFIED BY "******";
CREATE USER u_guest IDENTIFIED BY "******";
GRANT CREATE SESSION TO u_sales, u_guest;

-- 可視性ロールを付与
GRANT r_read_orders TO u_sales;
-- u_guest には付与しない

-- 実行検証
CONN u_sales/******
VARIABLE v NUMBER
EXEC :v := app.pkg_order_read.get_amount(1001);
PRINT v

CONN u_guest/******
-- 参照権限がないため実行時にORA-00942等のエラーが起こることを確認
EXEC app.pkg_order_read.get_amount(1001);

動的SQLとInvokerの相互作用

Invokerユニットで動的SQLを多用すると、実行時の権限と名前解決が動的に評価されるため、対象スキーマやテーブル名をパラメータにしてしまうと不要な権限漏洩リスクが高まる。完全修飾名とバインド変数を徹底し、オブジェクト名の入力を必要とする場合はホワイトリスト検証を行う。Definerユニットの内部でのみ動的DDLや権限付与を許容し、Invokerからは呼び出せない内部プロシージャに閉じておく。


CREATE OR REPLACE PACKAGE pkg_safe_exec AUTHID DEFINER AS
  PROCEDURE drop_temp_idx(p_table IN VARCHAR2);
END pkg_safe_exec;
/
CREATE OR REPLACE PACKAGE BODY pkg_safe_exec AS
  PROCEDURE drop_temp_idx(p_table IN VARCHAR2) IS
    v_owner  VARCHAR2(30) := 'DATA';
    v_name   VARCHAR2(128);
  BEGIN
    -- ホワイトリストの例(簡略)
    IF p_table NOT IN ('ORDERS') THEN
      RAISE_APPLICATION_ERROR(-20000, 'not allowed');
    END IF;
    v_name := v_owner||'.'||p_table||'_TMP_IDX';
    EXECUTE IMMEDIATE 'DROP INDEX '||DBMS_ASSERT.SIMPLE_SQL_NAME(v_name);
  END;
END pkg_safe_exec;
/

監査とトレーサビリティの確保

APPはDBMS_APPLICATION_INFOでMODULEとACTION、CLIENT IDENTIFIERを設定し、Unified AuditingやFGAの記録と突合できるようにする。Invokerであれば呼び出し元ユーザーとロールの情報がそのまま記録に残るため、誰がどの権限で実行したかの説明責任を満たしやすい。Definerで権限昇格する操作は監査ポリシで個別に捕捉し、API単位で相関IDを共有すると原因究明が迅速になる。


BEGIN
  DBMS_APPLICATION_INFO.set_module('ORDER_API','READ');
  DBMS_SESSION.set_identifier('CID-20251009-0001');
  -- 以降の呼び出しは同一の実行文脈で監査・ログと結合できる
  :x := app.pkg_order_read.get_amount(1001);
END;
/

移送と依存の管理、そしてまとめ

移送時にはDefinerユニットの依存権限がデプロイ先で満たされているかを事前に検証し、ALL_DEPENDENCIESでAPPとDATA間の参照を可視化して循環を排除しておく。Invokerユニットは呼び出し元の権限に依存するため、本番直前にロール構成と実行テストを必ず行い、想定外の権限不足や逆に過剰な可視性が生じていないかを確認する。更新はDefinerで統制し、参照はInvokerで可視性を委譲するという原則を守ることで、マルチスキーマの複雑さを抑えながらセキュアで監査可能なPL/SQLアーキテクチャを実現できる。