【PL/SQL】権限分離とセキュリティパッケージ(Invoker Rights活用例)

【PL/SQL】権限分離とセキュリティパッケージ(Invoker Rights活用例) PL/SQL

アプリケーションの安全性と最小権限の原則を両立させるには、スキーマ分離とPL/SQLのInvoker Rights(AUTHID CURRENT_USER)を軸にした「セキュリティパッケージ」設計が有効です。業務データを持つ実体スキーマと、APIを提供する実行スキーマを分け、APIは呼び出し者の権限で動かすべき部分と定義者権限でカプセル化すべき部分を切り分けます。ここでは権限分離の全体像、Invoker Rightsの適用箇所、Secure Definerとのハイブリッド、ロールや動的SQLの取り扱い、テストと運用までを実務に即して解説します。

権限分離の全体像とスキーマ配置

まずは表や索引などデータ実体を保持する「DATAスキーマ」と、PL/SQL APIを公開する「APIスキーマ」を分けます。外部クライアントやアプリユーザーにはAPIスキーマのパッケージに対するEXECUTEのみを与え、DATAスキーマの表権限はAPIスキーマへだけ直接GRANTします。こうすることでアクセス経路がAPIに一本化され、監査やロールバックが容易になります。

-- DATAスキーマ側(所有者のみフル権限)
CREATE TABLE data.orders (
  order_id    NUMBER PRIMARY KEY,
  cust_id     NUMBER NOT NULL,
  amount      NUMBER(12,2) NOT NULL,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- DATA→APIに最小権限を直接付与(ロール経由は不可)
GRANT SELECT, INSERT, UPDATE ON data.orders TO api;

Invoker Rightsの基本と「呼び出し者の権限で動く」API

Invoker RightsはAUTHID CURRENT_USERを指定したパッケージや関数が、呼び出し者のオブジェクト権限で実行される仕組みです。同一のAPIが複数テナントや部署横断で使われ、参照先表が呼び出し者ごとに異なる場合、あるいは呼び出し者の可視範囲をそのまま反映したいレポート系ユースケースに向きます。

-- APIスキーマ:呼び出し者の権限で動くレポート関数
CREATE OR REPLACE PACKAGE api.rep AUTHID CURRENT_USER AS
  FUNCTION total_amount(p_from DATE, p_to DATE) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY api.rep AS
  FUNCTION total_amount(p_from DATE, p_to DATE) RETURN NUMBER IS v NUMBER;
  BEGIN
    SELECT SUM(amount) INTO v
      FROM data.orders   -- 呼び出し者が持つSELECT権限で評価される
     WHERE created_at >= p_from AND created_at < p_to;
    RETURN NVL(v,0);
  END;
END;
/

-- 利用ユーザーに「表のSELECT」と「APIのEXECUTE」を直接付与
GRANT EXECUTE ON api.rep    TO app_user;
GRANT SELECT  ON data.orders TO app_user;

Secure Definerでのカプセル化と呼び出し者検証

業務ロジックを一括で実行し、内部で複数表を更新するコマンド系処理はAUTHID DEFINERのほうが安全です。過剰権限の“肩代わり”を避けるために入口で呼び出し元を検証し、必要最小のオブジェクト権限だけをAPIスキーマが持つ構成にします。動的SQLは必ずバインドを使い、列名やスキーマ名の外部入力をホワイトリストで制限します。

-- Secure Definerパターン(定義者権限でトランザクション処理)
CREATE OR REPLACE PACKAGE api.order_api AUTHID DEFINER AS
  PROCEDURE upsert_order(p_id NUMBER, p_cust NUMBER, p_amt NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY api.order_api AS
  PROCEDURE assert_caller IS
  BEGIN
    IF SYS_CONTEXT('USERENV','SESSION_USER') NOT IN ('APP_USER','BATCH_USER') THEN
      RAISE_APPLICATION_ERROR(-20000, 'not allowed');
    END IF;
  END;
  PROCEDURE upsert_order(p_id NUMBER, p_cust NUMBER, p_amt NUMBER) IS
  BEGIN
    assert_caller; -- 呼び出し者をホワイトリストで検証
    MERGE INTO data.orders d
    USING (SELECT p_id id, p_cust cid, p_amt amt FROM dual) s
       ON (d.order_id = s.id)
     WHEN MATCHED THEN UPDATE SET d.cust_id=s.cid, d.amount=s.amt
     WHEN NOT MATCHED THEN INSERT (order_id, cust_id, amount) VALUES (s.id, s.cid, s.amt);
  END;
END;
/
GRANT EXECUTE ON api.order_api TO app_user, batch_user;

InvokerとDefinerのハイブリッドで責務を分離する

読み取りはInvoker、更新はDefinerという層分けが現実的です。レポートや検索APIは呼び出し者の可視性に従わせ、書き込みや精緻な権限カプセル化が必要な操作はSecure Definerで提供します。ファサードとなる薄い仕様パッケージをAUTHID DEFINERで用意し、その内部でAUTHID CURRENT_USERのユーティリティを呼ぶことで、公開インターフェースを安定させつつ拡張性を保てます。

-- 薄いファサード(Definer)。内部でInvokerユーティリティを委譲
CREATE OR REPLACE PACKAGE api.facade AUTHID DEFINER AS
  FUNCTION report_sum(p_from DATE, p_to DATE) RETURN NUMBER;
  PROCEDURE apply_delta(p_id NUMBER, p_amt NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY api.facade AS
  FUNCTION report_sum(p_from DATE, p_to DATE) RETURN NUMBER IS
  BEGIN
    RETURN api.rep.total_amount(p_from, p_to); -- Invoker側へ委譲
  END;
  PROCEDURE apply_delta(p_id NUMBER, p_amt NUMBER) IS
  BEGIN
    api.order_api.upsert_order(p_id, NULL, p_amt); -- Secure Definerを呼ぶ
  END;
END;
/

ロールの扱いと直接GRANTの原則

PL/SQL実行時にはロール経由のオブジェクト権限が無効になる点に注意します。必要なSELECTやINSERTなどは必ず対象ユーザーへ直接GRANTします。EXECUTE権限も同様で、ロールではなく明示GRANTを徹底します。これを怠ると開発環境では動くのに本番でORA-00942となる典型障害に直結します。

セキュリティコンテキストとRLSとの連携

テナントIDやロール、所属部署などの属性をアプリケーションコンテキストに格納し、SYS_CONTEXTで参照すると分岐を簡潔に保てます。行レベルセキュリティ(DBMS_RLS)と併用する場合、Invoker Rightsの読み取り系APIはポリシーによる自動フィルタと親和性が高く、Definerの更新系ではUPDATE_CHECKで“別部門への移送”を塞ぐなどの設計が有効です。

CREATE CONTEXT app_ctx USING api.ctx_pkg;
CREATE OR REPLACE PACKAGE api.ctx_pkg AS PROCEDURE set_tenant(p_tid VARCHAR2); END;
/
CREATE OR REPLACE PACKAGE BODY api.ctx_pkg AS
  PROCEDURE set_tenant(p_tid VARCHAR2) IS
  BEGIN
    DBMS_SESSION.SET_CONTEXT('APP_CTX','TENANT_ID', p_tid);
  END;
END;
/
-- 参照側で利用
SELECT SUM(amount)
  FROM data.orders
 WHERE tenant_id = SYS_CONTEXT('APP_CTX','TENANT_ID');

動的SQL・プロキシユーザー・監査の実務ポイント

EXECUTE IMMEDIATEを使う場合でも文字列連結で値を埋め込まず、常にバインド変数で構築するとカーソル再利用と注入対策の両方に効きます。複数アプリが共通APIを使う際に接続元を識別したい場合はプロキシユーザーやDBMS_SESSION.SET_IDENTIFIERでトレーサビリティを担保します。監査は自治トランザクションの軽量ロガーを用意し、呼び出し者、入力キー、成功可否、経過時間を記録します。

CREATE OR REPLACE PROCEDURE api.audit(p_action VARCHAR2, p_key VARCHAR2) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  INSERT INTO api_audit(action, key_val, who, at)
  VALUES (p_action, p_key, SYS_CONTEXT('USERENV','SESSION_USER'), SYSTIMESTAMP);
  COMMIT;
END;
/

権限テストとデプロイの勘所

テストは「権限が足りないことを確認する」負のテストを含めると堅牢になります。API実行ユーザーに対して、ロールを一切使わず必要最小の直接GRANTだけで回るかを検証し、不要な権限は即時に剥奪します。デプロイでは仕様(Spec)変更を最小化し、BODY差し替えで改善を反復し、リリース後にUTL_RECOMPでINVALIDを収束させてからスモーク実行でウォームアップします。

よくある落とし穴と回避策

Invoker RightsのAPIが暗黙シノニムに依存していると、呼び出し者の検索パス差異で動作が変わります。完全修飾名か、アプリ専用の同名シノニムをユーザーごとに用意して挙動を固定します。DefinerのAPIに過剰なシステム権限を与えると横展開のスコープが広すぎるため、個々のオブジェクト権限に分解して持たせます。動的SQLで列名や表名を外部入力に通すのは厳禁で、ホワイトリスト検証とバインドで骨格を固定します。

まとめ

PL/SQLの権限分離は「DATAとAPIのスキーマ分離」「Invokerで可視範囲を自然に反映」「Secure Definerで状態変更を厳密にカプセル化」という三本柱で設計すると筋が通ります。ロールではなく直接GRANT、入口での呼び出し者検証、動的SQLの骨格固定、セキュリティコンテキストとの連携、自治トランザクション監査を揃えれば、最小権限と実装効率を両立したセキュアなデータアクセス層を構築できます。読み取りはInvoker、更新はDefiner、公開は薄いファサードという責務分離を守ることで、拡張に強く監査しやすいAPI基盤が完成します。