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

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

PL/SQLをマルチスキーマで運用すると、テーブル所有者、API所有者、接続ユーザー、バッチユーザーが分かれます。このとき AUTHID DEFINERAUTHID CURRENT_USER を曖昧に使うと、開発環境では動くのに本番だけ権限不足になる、逆に本来見せたくないデータを参照できる、という事故につながります。

この記事では、DATAAPPAPP_USER のような分離を前提に、Definer RightsとInvoker Rightsの使い分け、ロールと直接GRANT、名前解決、Oracle 12c以降で重要な INHERIT PRIVILEGES、動的SQLの検証までを実務向けに整理します。

先に結論

  • 更新系・権限昇格系は AUTHID DEFINER でAPI境界に閉じるのが基本です。
  • 呼び出し元ごとに見えるデータを変えたい参照系は AUTHID CURRENT_USER が候補になります。
  • Definer Rightsではロールに頼らず、所有者へ直接GRANTします。
  • Invoker Rightsでは呼び出し元の権限・名前解決・有効ロール・呼び出し経路を検証します。
  • Oracle 12c以降では INHERIT PRIVILEGES 不足による ORA-06598 も確認します。

AUTHID全般の基礎は AUTHIDと権限管理の設計、依存関係の確認は パッケージ間依存を可視化するメタデータ解析、監査と相関IDは ロギングとトレーサビリティを両立する監査設計 と合わせて読むと理解しやすくなります。

スポンサーリンク

スキーマ分離の基本形

ここでは、永続データを持つ DATA、PL/SQL APIを持つ APP、外部アプリケーションから接続する APP_USER を分けて考えます。

  • DATA: テーブル、ビュー、インデックスなどの所有者。直接ログインや直接更新は最小化します。
  • APP: パッケージ、ビュー、APIを所有するスキーマ。DATAへの必要権限を直接受けます。
  • APP_USER: アプリケーションや利用者が接続するスキーマ。APPの公開APIを実行します。
  • OPS / BATCH: 運用・バッチ用ユーザー。必要なAPIだけを実行します。
schema-separation.sql
-- 例示用。実運用ではパスワード、表領域、プロファイルを別途設計する。
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 TO data;
GRANT CREATE PROCEDURE, CREATE SYNONYM TO app;

-- DATAスキーマ
CREATE TABLE data.orders (
  order_id    NUMBER PRIMARY KEY,
  customer_id NUMBER NOT NULL,
  amount      NUMBER(12,2) NOT NULL,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);

-- Definer RightsのAPPパッケージが使う権限は直接GRANTする
GRANT SELECT, INSERT, UPDATE ON data.orders TO app;

-- API実行権限はAPPから接続ユーザーへ付与する
-- 実体パッケージ作成後に実行する
-- GRANT EXECUTE ON app.pkg_order_write TO app_user;

ロールは利用者の可視性を切り替えるには便利ですが、PL/SQLのコンパイルやDefiner Rightsでは直接GRANTが必要になります。マルチスキーマ設計では、「誰が所有するか」と「誰の権限で実行するか」を分けて考えます。

Definer RightsとInvoker Rightsの違い

AUTHID DEFINER は、パッケージ所有者の権限で実行されます。AUTHID CURRENT_USER は、呼び出し元のユーザーを基準に権限チェックと名前解決が行われます。ただし、ロール、静的SQL、動的SQL、呼び出し経路によって見え方が変わるため、単純に「Invokerならロールが全部使える」と覚えるのは危険です。

  • Definer Rights: 更新系、管理系、権限をAPI内に閉じたい処理に向きます。
  • Invoker Rights: 呼び出し元ごとの可視性や権限差を反映したい参照系に向きます。
  • 静的SQL: コンパイル時と実行時の権限要件を分けて確認します。
  • 動的SQL: 実行時の権限・名前解決・入力検証が重要です。

更新系はDefiner Rightsに閉じる

注文登録、顧客統合、締め戻し、データ補正のような更新系は、呼び出し元にテーブル権限を直接渡すより、APPのDefiner Rightsパッケージに閉じるほうが安全です。外部には業務APIだけを公開し、DATA表への直接更新を許可しない構成にします。

definer-rights-write-api.sql
CREATE OR REPLACE PACKAGE app.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 app.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 data.orders d
    USING (
      SELECT p_order_id AS order_id,
             p_customer_id AS customer_id,
             p_amount AS amount
      FROM dual
    ) s
    ON (d.order_id = s.order_id)
    WHEN MATCHED THEN
      UPDATE SET d.customer_id = s.customer_id,
                 d.amount = s.amount
    WHEN NOT MATCHED THEN
      INSERT (order_id, customer_id, amount)
      VALUES (s.order_id, s.customer_id, s.amount);
  END;
END pkg_order_write;
/

GRANT EXECUTE ON app.pkg_order_write TO app_user;

この構成では、APP_USERDATA.ORDERS のUPDATE権限を渡す必要がありません。必要なのは APP.PKG_ORDER_WRITE の実行権限です。権限昇格を許す代わりに、API内部で入力検証、監査ログ、例外処理を必ず行います。

参照系はInvoker Rightsで可視性を委譲する

呼び出し元の部署、テナント、ロール、VPDポリシーによって見える範囲を変えたい場合、Invoker Rightsが候補になります。ただし、Invoker Rightsの中で参照するオブジェクト名は、呼び出し元側で解決できる形にします。

invoker-rights-read-api.sql
CREATE OR REPLACE PACKAGE app.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 app.pkg_order_read AS
  FUNCTION get_amount(p_order_id IN NUMBER) RETURN NUMBER IS
    v_amount NUMBER;
  BEGIN
    -- Invoker Rightsでは名前解決が呼び出し元に寄るため、完全修飾を基本にする。
    SELECT amount
    INTO v_amount
    FROM data.orders
    WHERE order_id = p_order_id;

    RETURN v_amount;
  END;
END pkg_order_read;
/

GRANT EXECUTE ON app.pkg_order_read TO app_user;

Invoker Rightsでシノニムに頼る場合、APP側だけでなく呼び出し元側でも名前解決できるかを確認します。PUBLIC SYNONYMは便利ですが、名前衝突や環境差の原因にもなります。大規模環境では、重要な参照は完全修飾名に寄せたほうが調査しやすくなります。

Oracle 12c以降のINHERIT PRIVILEGESを確認する

Invoker Rightsを使う記事で特に落としやすいのが、Oracle 12c以降の INHERIT PRIVILEGES です。Invoker Rightsユニットの所有者が、呼び出し元ユーザーの権限を継承できない場合、ORA-06598: insufficient INHERIT PRIVILEGES privilege が発生します。

inherit-privileges-check.sql
-- APPがAPP_USERの権限を継承できるようにする例
GRANT INHERIT PRIVILEGES ON USER app_user TO app;

-- より広い権限。安易に付けず、管理者ロールや運用方針に合わせて判断する。
-- GRANT INHERIT ANY PRIVILEGES TO app;

-- 現在の継承権限を確認
SELECT grantee, owner, table_name, privilege
FROM dba_tab_privs
WHERE privilege = 'INHERIT PRIVILEGES'
  AND grantee = 'APP'
ORDER BY owner, table_name;

INHERIT ANY PRIVILEGES は強い権限です。全体付与で解決する前に、対象ユーザー単位の GRANT INHERIT PRIVILEGES ON USER ... で足りるかを確認します。移行時や本番だけInvoker Rightsが失敗する場合、この権限差を疑います。

ロールと直接GRANTを分けてテストする

Definer Rightsではロールに頼らず、所有者へ直接GRANTします。Invoker Rightsでは呼び出し元の権限差を反映できますが、静的SQL、動的SQL、呼び出し元、呼び出し経路を分けてテストします。

role-and-direct-grant-test.sql
-- 参照ロールを作り、APP_USERにだけ付与する例
CREATE ROLE r_read_orders;
GRANT SELECT ON data.orders TO r_read_orders;
GRANT r_read_orders TO app_user;

-- APP_USERで実行できることを確認
CONNECT app_user/******
SELECT app.pkg_order_read.get_amount(1001) FROM dual;

-- ロールを無効化して差分を見る
SET ROLE NONE;
SELECT app.pkg_order_read.get_amount(1001) FROM dual;

-- Definer Rightsの更新APIは、APP_USERがDATA.ORDERSを直接更新できなくても実行可能
EXEC app.pkg_order_write.upsert_order(1001, 10, 500);

このテストで見たいのは、「どの権限を誰に渡すと動くか」ではなく、「想定していない権限でも動いてしまわないか」です。参照系はロール差、更新系はAPI実行権限だけで動くことを確認します。

名前解決とシノニムの落とし穴

Invoker Rightsでは、未修飾名の解決が呼び出し元側に寄るため、開発環境でAPPに作ったシノニムだけを見て安心すると、本番のAPP_USERで失敗することがあります。特に同名テーブルやPUBLIC SYNONYMがある環境では、想定外のオブジェクトを参照するリスクがあります。

  • 重要なDATA表は data.orders のように完全修飾する
  • シノニムを使うなら環境ごとの差分を管理する
  • PUBLIC SYNONYMは命名衝突と影響範囲を確認する
  • Invokerユニットは複数ユーザーで実行テストする

動的SQLはQUALIFIED_SQL_NAMEで検証する

スキーマ修飾名を扱う場合、DBMS_ASSERT.SIMPLE_SQL_NAME ではなく DBMS_ASSERT.QUALIFIED_SQL_NAME を使います。DATA.ORDERS_TMP_IDX のような修飾名は、単純名ではないためです。

safe-dynamic-ddl.sql
CREATE OR REPLACE PACKAGE app.pkg_safe_exec AUTHID DEFINER AS
  PROCEDURE drop_temp_idx(p_table_name IN VARCHAR2);
END pkg_safe_exec;
/

CREATE OR REPLACE PACKAGE BODY app.pkg_safe_exec AS
  PROCEDURE drop_temp_idx(p_table_name IN VARCHAR2) IS
    v_index_name VARCHAR2(261);
  BEGIN
    -- 動的DDLはホワイトリストで対象を限定する
    IF UPPER(p_table_name) NOT IN ('ORDERS') THEN
      RAISE_APPLICATION_ERROR(-20000, 'not allowed target');
    END IF;

    v_index_name := 'DATA.' || UPPER(p_table_name) || '_TMP_IDX';

    EXECUTE IMMEDIATE
      'DROP INDEX ' || DBMS_ASSERT.QUALIFIED_SQL_NAME(v_index_name);
  END;
END pkg_safe_exec;
/

動的SQLでは、オブジェクト名はバインドできません。そのため、識別子を受け取る設計自体をできるだけ避け、必要な場合はホワイトリスト、DBMS_ASSERT、監査ログをセットにします。値は文字列連結せず、必ずバインド変数を使います。

監査と相関IDを残す

Definer Rightsで権限昇格する操作は、誰が呼んだのかを追えるようにします。DBMS_APPLICATION_INFODBMS_SESSION.SET_IDENTIFIER、アプリケーションログ、Unified AuditingやFGAを組み合わせると、APP所有パッケージ経由の操作でも呼び出し元を説明しやすくなります。

set-context-before-api-call.sql
BEGIN
  DBMS_APPLICATION_INFO.SET_MODULE(
    module_name => 'ORDER_API',
    action_name => 'READ'
  );

  DBMS_SESSION.SET_IDENTIFIER('CID-20260606-0001');

  -- APP_USERからAPPのAPIを呼ぶ
  :amount := app.pkg_order_read.get_amount(1001);
END;
/

権限設計はセキュリティだけでなく、障害調査にも効きます。誰の権限で、どのAPIを経由して、どのデータへ触れたかを後から追えるようにしておきます。

デプロイ前の権限・依存チェック

マルチスキーマでは、デプロイ先で権限が1つ足りないだけでINVALIDや実行時エラーになります。リリース前に、直接GRANT、ロール、INHERIT PRIVILEGES、依存関係、INVALIDを確認します。

pre-release-privilege-check.sql
-- APPがDATAへ直接持つ権限
SELECT owner, table_name, privilege
FROM all_tab_privs
WHERE grantee = 'APP'
  AND owner = 'DATA'
ORDER BY table_name, privilege;

-- APP_USERが実行できるAPP API
SELECT owner, table_name AS object_name, privilege
FROM all_tab_privs
WHERE grantee = 'APP_USER'
  AND owner = 'APP'
  AND privilege = 'EXECUTE'
ORDER BY object_name;

-- APPスキーマのINVALID
SELECT object_type, object_name, status
FROM all_objects
WHERE owner = 'APP'
  AND status = 'INVALID'
ORDER BY object_type, object_name;

-- APPからDATAへの依存
SELECT owner, name, type, referenced_owner, referenced_name, referenced_type
FROM all_dependencies
WHERE owner = 'APP'
  AND referenced_owner = 'DATA'
ORDER BY name, referenced_type, referenced_name;

依存の読み方や再コンパイル順序は、依存可視化とINVALID再コンパイルの記事で詳しく扱っています。権限不足はコンパイル時に出る場合と、Invoker Rightsの実行時に初めて出る場合があるため、代表ユーザーでの実行テストを必ず入れます。

運用チェックリスト

  • 更新系APIはDefiner Rightsに閉じているか
  • 参照系Invoker Rightsの名前解決を完全修飾または明示的なシノニムで管理しているか
  • APPへのDATA権限は直接GRANTされているか
  • Invoker Rightsで必要なロール差を複数ユーザーで検証しているか
  • INHERIT PRIVILEGES または INHERIT ANY PRIVILEGES の方針があるか
  • 動的SQLの識別子を DBMS_ASSERT とホワイトリストで検証しているか
  • 権限昇格APIの呼び出し元を監査できるか
  • 本番デプロイ前にINVALIDと依存関係を確認しているか

まとめ

マルチスキーマ環境のPL/SQL権限設計では、Definer RightsとInvoker Rightsを役割で分けることが重要です。更新系や権限昇格が必要な処理はDefiner Rightsに閉じ、呼び出し元ごとの可視性を反映したい参照系はInvoker Rightsを検討します。

ただし、Invoker Rightsでは名前解決、ロール、直接権限、Oracle 12c以降の INHERIT PRIVILEGES が絡みます。動的SQLは最終手段にし、使う場合は DBMS_ASSERT.QUALIFIED_SQL_NAME とホワイトリストで守ります。権限・依存・監査をセットで設計すれば、マルチスキーマでも保守しやすく、説明責任を果たせるPL/SQL APIにできます。