【PL/SQL】権限分離とセキュリティパッケージ設計|Invoker Rights・直接GRANT・RLS

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

PL/SQLで業務APIを作るとき、すべてのユーザーに表権限を直接配る設計は危険です。一方で、すべてを高権限のDefiner Rightsパッケージに閉じ込めると、呼び出し者ごとの可視範囲や監査が曖昧になります。実務では、DATAスキーマとAPIスキーマを分け、読み取り・更新・監査・行レベル制御を分担させる設計が安定します。

この記事では、AUTHID CURRENT_USERAUTHID DEFINER、直接GRANT、INHERIT PRIVILEGESDBMS_SESSION.SET_CONTEXTDBMS_RLS、動的SQL、監査ログを組み合わせたセキュリティパッケージ設計を整理します。AUTHIDの基礎は AUTHIDと権限管理の設計、Invoker / Definerの詳しい使い分けは Invoker RightsとDefiner Rights完全ガイド もあわせて確認してください。

この記事で扱うこと

  • DATAスキーマとAPIスキーマを分ける理由
  • Invoker RightsとDefiner Rightsの判断フロー
  • 直接GRANTとロール無効化の注意点
  • INHERIT PRIVILEGESORA-06598 の考え方
  • DBMS_SESSION.SET_CONTEXTDBMS_RLS の連携
  • 動的SQL、監査ログ、負の権限テスト、本番前チェック
スポンサーリンク

最初に結論:読み取りはInvoker、更新はSecure Definer

権限分離で迷ったら、まず読み取りはInvoker Rights、更新はSecure Definerを基本形にします。呼び出し者の可視範囲を反映したい検索・レポートは AUTHID CURRENT_USER、複数表を一貫して更新する業務処理は AUTHID DEFINER でカプセル化する、という分け方です。

Invoker Rights呼び出し者の権限・名前解決を反映したい検索、レポート、テナント別参照に向きます。
Definer RightsAPI所有者の権限で、内部表を隠しながら業務更新を提供したい場面に向きます。
Secure DefinerDefiner Rightsの入口で呼び出し元、テナント、操作種別を検証してから処理する設計です。
RLS / VPD行レベルの絞り込みをアプリごとのWHERE句ではなくDBポリシーへ寄せたい場合に使います。

DATAスキーマとAPIスキーマを分ける

まず、表や索引を持つDATAスキーマと、PL/SQL APIを公開するAPIスキーマを分けます。アプリケーションユーザーには、DATAスキーマの表を直接更新させず、APIスキーマのパッケージに対する EXECUTE を与えます。

スキーマ分離と直接GRANTの例

schema-grant.sql
-- DATAスキーマ: 実データを保持
CREATE TABLE data.orders (
  order_id    NUMBER PRIMARY KEY,
  tenant_id   VARCHAR2(30) NOT NULL,
  cust_id     NUMBER NOT NULL,
  amount      NUMBER(12,2) NOT NULL,
  status      VARCHAR2(20) NOT NULL,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- APIスキーマへ必要最小限のオブジェクト権限を直接付与
GRANT SELECT, INSERT, UPDATE ON data.orders TO api;

-- アプリユーザーにはAPIだけ実行させる
GRANT EXECUTE ON api.order_api TO app_user;

ユーザー・権限・ロールの基本は Oracleユーザー・権限・ロール完全ガイド、権限不足の切り分けは ORA-01031 insufficient privilegesの原因と対処ORA-00942の原因と解決方法 が関連します。

Invoker Rightsの基本

AUTHID CURRENT_USER を指定したPL/SQLは、実行時の呼び出し者を基準に名前解決と権限チェックが行われます。呼び出し者ごとに見える表やシノニムを変えたい場合、または可視範囲をそのまま反映したいレポートAPIに向きます。

呼び出し者の権限で集計する例

invoker-report.sql
CREATE OR REPLACE PACKAGE api.report_api
  AUTHID CURRENT_USER
AS
  FUNCTION total_amount(
    p_from IN DATE,
    p_to   IN DATE
  ) RETURN NUMBER;
END;
/

CREATE OR REPLACE PACKAGE BODY api.report_api AS
  FUNCTION total_amount(
    p_from IN DATE,
    p_to   IN DATE
  ) RETURN NUMBER IS
    v_total NUMBER;
  BEGIN
    SELECT SUM(amount)
      INTO v_total
      FROM data.orders
     WHERE created_at >= p_from
       AND created_at <  p_to;

    RETURN NVL(v_total, 0);
  END;
END;
/

Invoker Rightsでは、呼び出し者側に必要な権限やシノニムがないと失敗します。「APIを実行できること」と「API内部で参照する表を読めること」は分けて確認します。

Definer Rightsで更新処理をカプセル化する

AUTHID DEFINER はデフォルトの動作です。API所有者の権限で処理するため、利用者に表の更新権限を渡さず、パッケージ実行権限だけで業務処理を提供できます。ただし、高権限の肩代わりになるため、入口で呼び出し元や操作範囲を検証します。

Secure Definerの更新API

secure-definer-api.sql
CREATE OR REPLACE PACKAGE api.order_api
  AUTHID DEFINER
AS
  PROCEDURE approve_order(
    p_order_id IN NUMBER,
    p_tenant_id IN VARCHAR2
  );
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 approve_order(
    p_order_id IN NUMBER,
    p_tenant_id IN VARCHAR2
  ) IS
  BEGIN
    assert_caller;

    UPDATE data.orders
       SET status = 'APPROVED'
     WHERE order_id = p_order_id
       AND tenant_id = p_tenant_id
       AND status = 'WAITING';

    IF SQL%ROWCOUNT = 0 THEN
      RAISE_APPLICATION_ERROR(-20001, 'order not found or not allowed');
    END IF;
  END;
END;
/

更新APIでは、呼び出し者だけでなく、テナントID、業務状態、変更可能範囲を必ず条件に入れます。表権限を隠しても、APIが何でも更新できる入口になってしまえば危険です。

判断フロー:InvokerかDefinerか

どちらを使うべきかは、処理の目的で決めます。以下のように考えると、大きく外しにくくなります。

呼び出し者の見える範囲で検索したいAUTHID CURRENT_USER が候補です。ユーザー別シノニムやRLSと相性があります。
表権限を隠して更新APIだけ公開したいAUTHID DEFINER が候補です。API内で入力検証と操作範囲制限を行います。
共通APIを複数スキーマから使いたいInvoker Rightsで名前解決を呼び出し者側へ寄せるか、完全修飾名で固定するかを決めます。
外部入力で表名・列名を切り替えたい動的SQLが必要です。ただし値はバインド、識別子はホワイトリストで検証します。
行レベルで自動的に絞り込みたいアプリケーションコンテキストとDBMS_RLSを組み合わせます。

マルチスキーマ環境での設計は マルチスキーマ環境での権限管理とInvoker Rights設計 も参考になります。

ロールではなく直接GRANTを確認する

Definer Rightsの名前付きPL/SQLでは、ロール経由の権限に頼れません。開発環境ではロールのおかげでSELECTできても、パッケージをコンパイル・実行すると失敗することがあります。必要なオブジェクト権限は、所有者やAPIスキーマへ直接GRANTします。

直接GRANTの確認SQL

direct-grant-check.sql
-- 自分に直接付与されたオブジェクト権限
SELECT owner,
       table_name,
       privilege,
       grantor
  FROM user_tab_privs
 ORDER BY owner, table_name, privilege;

-- APIスキーマに付与されている権限を確認
SELECT owner,
       table_name,
       privilege,
       grantee
  FROM all_tab_privs
 WHERE grantee = 'API'
 ORDER BY owner, table_name, privilege;

コンパイルや依存関係が絡む場合は 依存オブジェクトとINVALID再コンパイル も確認してください。権限不足が原因でINVALID化し、再コンパイルしても直らないケースがあります。

INHERIT PRIVILEGESとORA-06598

Invoker Rightsでは、呼び出し者の権限を実行側が利用するため、INHERIT PRIVILEGES の制御が関係します。環境によっては、呼び出し者の権限を継承できず ORA-06598 が発生することがあります。誰の権限を誰が継承してよいのかを明示的に管理します。

INHERIT PRIVILEGESの例

inherit-privileges.sql
-- apiユーザーがapp_userの権限を継承してInvoker Rightsを実行できるようにする例
GRANT INHERIT PRIVILEGES ON USER app_user TO api;

-- 逆に不用意な継承を避けたい場合は取り消す
REVOKE INHERIT PRIVILEGES ON USER app_user FROM api;

INHERIT PRIVILEGES はセキュリティ境界そのものです。エラーを消すために広く付与するのではなく、どのAPI所有者にどの呼び出し者の権限継承を許すかを設計します。

セキュリティコンテキストでテナントを保持する

テナントID、部署、利用者区分などを毎回API引数で渡すと、漏れや改ざんの余地が増えます。接続時やリクエスト開始時に DBMS_SESSION.SET_CONTEXT でコンテキストへ格納し、SQL側では SYS_CONTEXT で参照する設計が有効です。

DBMS_SESSION.SET_CONTEXTの例

security-context.sql
CREATE CONTEXT app_ctx USING api.ctx_pkg;

CREATE OR REPLACE PACKAGE api.ctx_pkg
  AUTHID DEFINER
AS
  PROCEDURE set_tenant(p_tenant_id IN VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY api.ctx_pkg AS
  PROCEDURE set_tenant(p_tenant_id IN VARCHAR2) IS
  BEGIN
    DBMS_SESSION.SET_CONTEXT(
      namespace => 'APP_CTX',
      attribute => 'TENANT_ID',
      value     => p_tenant_id
    );
  END;
END;
/

SELECT SYS_CONTEXT('APP_CTX', 'TENANT_ID') AS tenant_id
  FROM dual;

セキュリティコンテキストの実装は DBMS_SESSION.SET_CONTEXTの使い方 に詳しくまとめています。

RLS / VPDで行レベル制御をDBへ寄せる

アプリ側で毎回 tenant_id = :tenant_id を書く運用は、漏れた瞬間に情報漏えいにつながります。行レベルの絞り込みをDBポリシーとして強制したい場合は、DBMS_RLS によるVPDを検討します。

DBMS_RLSポリシーの骨格

rls-policy.sql
CREATE OR REPLACE FUNCTION api.order_policy(
  p_schema IN VARCHAR2,
  p_object IN VARCHAR2
) RETURN VARCHAR2
AS
BEGIN
  RETURN 'tenant_id = SYS_CONTEXT(''APP_CTX'', ''TENANT_ID'')';
END;
/

BEGIN
  DBMS_RLS.ADD_POLICY(
    object_schema   => 'DATA',
    object_name     => 'ORDERS',
    policy_name     => 'ORDERS_TENANT_POLICY',
    function_schema => 'API',
    policy_function => 'ORDER_POLICY',
    statement_types => 'SELECT,UPDATE,DELETE',
    update_check    => TRUE
  );
END;
/

PL/SQL側のDBMS_RLS実装は DBMS_RLSの使い方、Oracle全体のVPD設計は Oracle VPD完全ガイド が関連します。

動的SQLは値をバインドし、識別子をホワイトリスト化する

セキュリティパッケージで動的SQLを使う場合、値を文字列連結してはいけません。値はバインド変数、表名・列名・ソートキーのような識別子はホワイトリストで検証します。Invoker Rightsと動的SQLを組み合わせると、名前解決や権限チェックも呼び出し者に寄るため、意図しない参照先に注意します。

ホワイトリスト付き動的SQL

dynamic-sql-whitelist.sql
CREATE OR REPLACE FUNCTION api.count_by_status(
  p_table_name IN VARCHAR2,
  p_status     IN VARCHAR2
) RETURN NUMBER
  AUTHID DEFINER
AS
  v_sql   VARCHAR2(1000);
  v_count NUMBER;
BEGIN
  IF p_table_name NOT IN ('DATA.ORDERS', 'DATA.ORDER_ARCHIVE') THEN
    RAISE_APPLICATION_ERROR(-20010, 'invalid table');
  END IF;

  v_sql := 'SELECT COUNT(*) FROM ' || p_table_name ||
           ' WHERE status = :status';

  EXECUTE IMMEDIATE v_sql INTO v_count USING p_status;
  RETURN v_count;
END;
/

動的SQLとバインド変数の詳細は 動的SQLとBIND変数の最適設計 を参照してください。

監査ログは本体処理と分ける

権限分離APIでは、誰が、どのAPIを、どのキーで、成功・失敗のどちらで実行したかを残します。本体処理がロールバックされても障害調査ログを残したい場合は、監査専用の自律トランザクションを検討します。

監査ログ用パッケージ

audit-package.sql
CREATE OR REPLACE PACKAGE api.audit_pkg
  AUTHID DEFINER
AS
  PROCEDURE write_log(
    p_action IN VARCHAR2,
    p_key    IN VARCHAR2,
    p_result IN VARCHAR2
  );
END;
/

CREATE OR REPLACE PACKAGE BODY api.audit_pkg AS
  PROCEDURE write_log(
    p_action IN VARCHAR2,
    p_key    IN VARCHAR2,
    p_result IN VARCHAR2
  ) IS
    PRAGMA AUTONOMOUS_TRANSACTION;
  BEGIN
    INSERT INTO api.audit_log(
      action_name,
      key_value,
      result_code,
      session_user,
      current_user,
      client_identifier,
      logged_at
    ) VALUES (
      p_action,
      p_key,
      p_result,
      SYS_CONTEXT('USERENV', 'SESSION_USER'),
      SYS_CONTEXT('USERENV', 'CURRENT_USER'),
      SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER'),
      SYSTIMESTAMP
    );

    COMMIT;
  END;
END;
/

監査ログは便利ですが、業務データの更新を自律トランザクションに逃がすのは避けます。ログは調査用、本体更新は通常トランザクションで扱う、という役割分担が安全です。

権限テストは成功ケースだけでは足りない

セキュリティ設計では、実行できることだけでなく、実行できてはいけないことが拒否されることをテストします。直接表SELECTできない、別テナント行を更新できない、ロールだけではコンパイルできない、未許可ユーザーがAPIを実行できない、という負のテストを入れます。

負の権限テスト例

negative-privilege-test.sql
-- app_userでは表を直接読めないこと
CONNECT app_user/password
SELECT COUNT(*) FROM data.orders;
-- ORA-00942 または権限不足になることを確認

-- API経由なら許可範囲だけ処理できること
BEGIN
  api.order_api.approve_order(
    p_order_id  => 1001,
    p_tenant_id => 'TENANT_A'
  );
END;
/

-- 別テナントは拒否されること
BEGIN
  api.order_api.approve_order(
    p_order_id  => 2001,
    p_tenant_id => 'TENANT_B'
  );
END;
/

プロシージャやファンクション自体の設計は PL/SQLプロシージャ・ファンクション完全ガイド も参考になります。

避けたい設計

全ユーザーに表権限を配るAPIを通さず直接更新され、監査・入力検証・業務ルールが抜け落ちます。
Definer APIに過剰権限を持たせるAPIの入口チェックが甘いと、広い権限を利用者に肩代わりさせる危険な窓口になります。
ロール前提でコンパイルする開発環境では動いても、本番の名前付きPL/SQLで権限不足になることがあります。
動的SQLに外部入力を直結するSQLインジェクション、カーソル共有阻害、意図しないオブジェクト参照につながります。
テナント条件をアプリ任せにするWHERE句の付け忘れが情報漏えいになります。RLSやAPI内検証へ寄せます。

本番前チェックリスト

  • DATAスキーマとAPIスキーマの責務が分かれている
  • アプリユーザーにDATA表の不要な直接権限がない
  • APIスキーマには必要最小限の直接GRANTだけがある
  • 読み取りAPIと更新APIでInvoker / Definerの使い分け理由が説明できる
  • Definer APIの入口で呼び出し元・テナント・業務状態を検証している
  • INHERIT PRIVILEGES の付与範囲を確認している
  • RLSやセキュリティコンテキストの初期化漏れをテストしている
  • 動的SQLの値はバインド、識別子はホワイトリストになっている
  • 成功テストだけでなく、拒否されるべき操作の負のテストがある
  • 監査ログに呼び出し者、対象キー、結果、時刻が残る

まとめ

PL/SQLの権限分離は、DATAスキーマとAPIスキーマを分け、表へ直接触らせず、API経由に集約するところから始まります。読み取りはInvoker Rightsで呼び出し者の可視範囲を反映し、更新はSecure Definerで業務ルールと監査をカプセル化するのが実務的です。

そのうえで、直接GRANT、INHERIT PRIVILEGES、セキュリティコンテキスト、DBMS_RLS、動的SQLのホワイトリスト、負の権限テストを組み合わせます。最小権限を守るだけでなく、誰が何をできて、何をできないのかをテストで証明できる状態にすることが、セキュリティパッケージ設計のゴールです。