PostgreSQLの権限管理とは、「ロール(ユーザーやグループ)」に対して、データベース・スキーマ・テーブル・列などの対象ごとに GRANT で操作の許可を与える仕組みです。 権限は「データベースに接続できるか」「スキーマの中を見られるか」「テーブルを読めるか」と階層になっており、どこか1段でも足りないと permission denied になります。

この記事では、権限の階層と対象別の権限一覧表、readonly / readwrite / migration の3つのグループロールとログインユーザーを作る設計テンプレートSQL一式(PostgreSQL 16.13 で実行し、\du・\dp・\dn+ の出力を掲載)、そして 「GRANT したのに permission denied」になる原因を12パターン実際に再現した結果と直し方をまとめます。PG15 の public スキーマの変更、PG16 のロール付与オプション、PG17・PG18 で増えた定義済みロールまで、今のバージョンに合わせて解説します。

PostgreSQL権限の早見表

やりたいこと SQL
グループ用ロールを作る(ログイン不可) CREATE ROLE app_readonly NOLOGIN;
ログインユーザーを作りグループに入れる CREATE ROLE report_user LOGIN PASSWORD '...' IN ROLE app_readonly;
後からグループに入れる GRANT app_readonly TO report_user;
DBへの接続を許可 GRANT CONNECT ON DATABASE appdb TO app_readonly;
スキーマの中を参照可能にする GRANT USAGE ON SCHEMA app TO app_readonly;
既存テーブルを全部読み取り可に GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readonly;
今後作るテーブルも自動で読み取り可に ALTER DEFAULT PRIVILEGES FOR ROLE 作成者 IN SCHEMA app GRANT SELECT ON TABLES TO app_readonly;
serial / シーケンスを使うINSERTを許可 GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_readwrite;
特定の列だけ読ませる GRANT SELECT (task_id, title) ON app.tasks TO role;
関数の実行を許可 GRANT EXECUTE ON FUNCTION app.f() TO role;
全テーブルを読み取り可(定義済みロール) GRANT pg_read_all_data TO auditor;(PG14〜)
権限を取り消す REVOKE SELECT ON app.tasks FROM role;
ロールを削除する REASSIGN OWNED BY old TO new; → DROP OWNED BY old; → DROP ROLE old;
権限を確認する \dp スキーマ.*、\dn+、\ddp、\drg(PG16〜)、has_table_privilege()

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQLのロールと権限|GRANT設計テンプレートとエラー対処」

ロールと権限の基本

ユーザーもグループも「ロール」

PostgreSQL には「ユーザー」と「グループ」の区別がなく、どちらもロールです。 LOGIN 属性を持つロールがユーザー、持たないロールがグループの役割をします。CREATE USER は CREATE ROLE ... LOGIN の別名です。

属性 意味 既定
LOGIN / NOLOGIN ログインできるか CREATE ROLE は NOLOGIN、CREATE USER は LOGIN
SUPERUSER 全ての権限チェックを通過 なし
CREATEDB データベースを作成できる なし
CREATEROLE ロールを作成できる(PG16で範囲が整理された) なし
INHERIT / NOINHERIT 所属グループの権限を自動で使えるか INHERIT
BYPASSRLS 行レベルセキュリティを無視 なし
CONNECTION LIMIT n 同時接続数の上限 -1(無制限)

ロール属性(CREATEDB など)は GRANT では付けられず、ALTER ROLE で設定します。 GRANT で扱うのは「オブジェクトへの権限」と「ロールへの所属」です。

権限は階層で効く

flowchart LR
    A[ロール] -->|CONNECT| B[データベース]
    B -->|USAGE| C[スキーマ]
    C -->|SELECT / INSERT ...| D[テーブル・ビュー]
    D -->|列単位の SELECT / UPDATE| E[列]
    C -->|USAGE / SELECT| F[シーケンス]
    C -->|EXECUTE| G[関数]

テーブルを読むには「DB の CONNECT」「スキーマの USAGE」「テーブルの SELECT」の3つが全てそろう必要があります。 GRANT SELECT ON テーブル だけでは足りないのが、つまずきやすい一番のポイントです。

対象別の権限一覧表

対象 付けられる権限 \dp 等での略号 PUBLIC の既定
データベース CONNECT / CREATE(スキーマ作成) / TEMPORARY c / C / T CONNECT・TEMPORARY あり
スキーマ USAGE(中を参照) / CREATE(中にオブジェクト作成) U / C public スキーマは PG15〜 USAGE のみ
テーブル・ビュー SELECT / INSERT / UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER / MAINTAIN(PG17〜) r / a / w / d / D / x / t / m なし
列 SELECT / INSERT / UPDATE / REFERENCES r / a / w / x なし
シーケンス USAGE(nextval) / SELECT(currval) / UPDATE(setval) U / r / w なし
関数・プロシージャ EXECUTE X あり
型・ドメイン USAGE U あり
設定パラメータ SET / ALTER SYSTEM(PG15〜) s / A なし
ロール 所属(GRANT ロール TO ロール)+ ADMIN / INHERIT / SET オプション(PG16〜) ― ―

\dp の t88_readwrite=arwd/t88_migration は「t88_readwrite に INSERT(a)・SELECT(r)・UPDATE(w)・DELETE(d) を、t88_migration が付与した」という意味です。ALL PRIVILEGES を付けた場合は arwdDxt(PG17以降は m を含む)と表示されます。

オブジェクトの所有者は特別

テーブルを作ったロール(所有者)は全ての権限を持ち、ALTER TABLE・DROP TABLE は所有者(またはそのグループのメンバー)にしかできません。 これは GRANT では渡せないため、「マイグレーション用のロールに所有させる」設計が大切になります。

ロール設計テンプレート(readonly / readwrite / migration)

ログインするユーザーに直接権限を付けず、NOLOGIN のグループロールに権限を集め、ユーザーはグループに所属させるのが基本形です。人やアプリが増えても GRANT グループ TO 新ユーザー の1行で済みます。

グループロール(NOLOGIN) 役割 主な権限 所属させるユーザー例
t88_readonly 参照のみ CONNECT・スキーマUSAGE・SELECT 集計・レポート用
t88_readwrite アプリの読み書き + INSERT/UPDATE/DELETE、シーケンス USAGE、関数 EXECUTE アプリの接続ユーザー
t88_migration テーブルの作成・変更 スキーマとテーブルの所有者 マイグレーションツール

以下は PostgreSQL 16.13 で実行した完全なスクリプトです。データベース t88 は作成済み、スーパーユーザー(postgres)で実行しています。名前の t88_ は自分の環境に合わせて置き換えてください。

-- 1) グループロール(ログイン不可)
CREATE ROLE t88_readonly  NOLOGIN;
CREATE ROLE t88_readwrite NOLOGIN;
CREATE ROLE t88_migration NOLOGIN;

-- 2) ログインユーザー(パスワードは必ず変更する)
CREATE ROLE t88_report_user LOGIN PASSWORD 'change-me-1' IN ROLE t88_readonly;
CREATE ROLE t88_app_user    LOGIN PASSWORD 'change-me-2' IN ROLE t88_readwrite;
CREATE ROLE t88_migrator    LOGIN PASSWORD 'change-me-3' IN ROLE t88_migration;

-- 3) データベース:PUBLIC の既定権限を外し、グループにだけ接続を許可
REVOKE ALL ON DATABASE t88 FROM PUBLIC;
GRANT CONNECT, TEMPORARY ON DATABASE t88 TO t88_readonly, t88_readwrite, t88_migration;

-- 4) スキーマ:所有者を migration にし、他は USAGE だけ
CREATE SCHEMA app AUTHORIZATION t88_migration;
GRANT USAGE ON SCHEMA app TO t88_readonly, t88_readwrite;

-- 5) 既にあるオブジェクトへの権限
GRANT SELECT ON ALL TABLES IN SCHEMA app TO t88_readonly;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO t88_readwrite;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO t88_readwrite;

-- 6) これから t88_migration が作るオブジェクトへの既定権限
ALTER DEFAULT PRIVILEGES FOR ROLE t88_migration IN SCHEMA app
  GRANT SELECT ON TABLES TO t88_readonly;
ALTER DEFAULT PRIVILEGES FOR ROLE t88_migration IN SCHEMA app
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO t88_readwrite;
ALTER DEFAULT PRIVILEGES FOR ROLE t88_migration IN SCHEMA app
  GRANT USAGE, SELECT ON SEQUENCES TO t88_readwrite;
ALTER DEFAULT PRIVILEGES FOR ROLE t88_migration IN SCHEMA app
  GRANT EXECUTE ON FUNCTIONS TO t88_readwrite;

マイグレーションは「SET ROLE してから」作る

ここで1つ目の落とし穴がありました。最初に t88_migrator(ログインユーザー)のまま CREATE TABLE したところ、アプリユーザーから読めませんでした。

SET ROLE t88_migrator;
CREATE TABLE app.tasks (task_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, owner_name text);
RESET ROLE;
SET ROLE t88_app_user;
INSERT INTO app.tasks (title) VALUES ('備品を発注する');
ERROR:  permission denied for table tasks

\dp を見ると、Access privileges が空でした。

 Schema |       Name        |   Type   | Access privileges | Column privileges | Policies
--------+-------------------+----------+-------------------+-------------------+----------
 app    | tasks             | table    |                   |                   |
 app    | tasks_task_id_seq | sequence |                   |                   |

ALTER DEFAULT PRIVILEGES FOR ROLE t88_migration は「t88_migration が作ったオブジェクト」にだけ働きます。 テーブルの所有者が t88_migrator になったため、既定権限が適用されませんでした。所有者をそろえるため、マイグレーションの最初に SET ROLE します。

SET ROLE t88_migrator;      -- 実際には t88_migrator でログインしている状態
SET ROLE t88_migration;     -- グループロールに切り替えてから DDL
SELECT current_user, session_user;
CREATE TABLE app.tasks (task_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, owner_name text);
CREATE TABLE app.items (item_id serial PRIMARY KEY, name text);
CREATE FUNCTION app.task_count() RETURNS bigint LANGUAGE sql AS 'SELECT count(*) FROM app.tasks';
RESET ROLE;
 current_user  | session_user
---------------+--------------
 t88_migration | postgres

(session_user が postgres なのは、検証をスーパーユーザーの接続から SET ROLE で行っているためです。実際に t88_migrator でログインした場合は t88_migrator になります。)

マイグレーションツールの多くは接続直後に任意のSQLを実行する設定を持っているので、そこに SET ROLE t88_migration を入れておくと、所有者が常にそろいます。 ログインユーザー側に ALTER ROLE t88_migrator SET role = 't88_migration'; を設定する方法もあります。

実行後の \du・\drg・\dn+・\dp・\ddp

         List of roles
    Role name    |  Attributes
-----------------+--------------
 t88_app_user    |
 t88_migration   | Cannot login
 t88_migrator    |
 t88_readonly    | Cannot login
 t88_readwrite   | Cannot login

PG16 の \du には「Member of」列がなくなり、所属は新しい \drg で確認します。

                    List of role grants
    Role name    |   Member of   |   Options    | Grantor
-----------------+---------------+--------------+----------
 t88_app_user    | t88_readwrite | INHERIT, SET | postgres
 t88_migrator    | t88_migration | INHERIT, SET | postgres
 t88_report_user | t88_readonly  | INHERIT, SET | postgres

スキーマの権限(\dn+ app):

 Name |     Owner     |       Access privileges        | Description
------+---------------+--------------------------------+-------------
 app  | t88_migration | t88_migration=UC/t88_migration+|
      |               | t88_readonly=U/t88_migration  +|
      |               | t88_readwrite=U/t88_migration  |

テーブルとシーケンスの権限(\dp app.*):

 Schema |       Name        |   Type   |          Access privileges          | Column privileges | Policies
--------+-------------------+----------+-------------------------------------+-------------------+----------
 app    | items             | table    | t88_readonly=r/t88_migration       +|                   |
        |                   |          | t88_readwrite=arwd/t88_migration   +|                   |
        |                   |          | t88_migration=arwdDxt/t88_migration |                   |
 app    | items_item_id_seq | sequence | t88_readwrite=rU/t88_migration     +|                   |
        |                   |          | t88_migration=rwU/t88_migration     |                   |
 app    | tasks             | table    | t88_readonly=r/t88_migration       +|                   |
        |                   |          | t88_readwrite=arwd/t88_migration   +|                   |
        |                   |          | t88_migration=arwdDxt/t88_migration |                   |
 app    | tasks_task_id_seq | sequence | t88_readwrite=rU/t88_migration     +|                   |
        |                   |          | t88_migration=rwU/t88_migration     |                   |

既定権限(\ddp):

     Owner     | Schema |   Type   |        Access privileges
---------------+--------+----------+----------------------------------
 t88_migration | app    | function | t88_readwrite=X/t88_migration
 t88_migration | app    | sequence | t88_readwrite=rU/t88_migration
 t88_migration | app    | table    | t88_readonly=r/t88_migration    +
               |        |          | t88_readwrite=arwd/t88_migration

動作確認

SET ROLE t88_app_user;
INSERT INTO app.tasks (title) VALUES ('備品を発注する') RETURNING *;
INSERT INTO app.items (name) VALUES ('ペン') RETURNING *;
SELECT app.task_count();
RESET ROLE;

SET ROLE t88_report_user;
SELECT * FROM app.tasks;
UPDATE app.tasks SET title = 'x';
RESET ROLE;
 task_id |     title      | owner_name
---------+----------------+------------
       1 | 備品を発注する |

 item_id | name
---------+------
       1 | ペン

 task_count
------------
          1

-- t88_report_user
 task_id |     title      | owner_name
---------+----------------+------------
       1 | 備品を発注する |
ERROR:  permission denied for table tasks

readwrite は読み書きと関数の実行ができ、readonly は SELECT だけが通ることを確認できました。 実際のログイン(psql -h 127.0.0.1 -U t88_app_user -d t88)でも同じ結果になります。

「GRANTしたのにpermission denied」原因一覧と直し方

PostgreSQL 16.13 で、以下を全て再現しました。エラー文の「for 〇〇」の部分を見ると、どの階層の権限が足りないかがわかります。

# エラー文 原因 直し方
1 FATAL: permission denied for database "t88" / User does not have CONNECT privilege. DB の CONNECT がない GRANT CONNECT ON DATABASE t88 TO role;
2 permission denied for schema app スキーマの USAGE がない GRANT USAGE ON SCHEMA app TO role;
3 permission denied for table notes(後から作ったテーブル) ON ALL TABLES は実行時点の既存分だけ ALTER DEFAULT PRIVILEGES を設定
4 permission denied for table ...(既定権限を設定済みなのに) 作成者が FOR ROLE と違う DDL 前に SET ROLE 所有者ロール
5 permission denied for schema public(CREATE TABLE 時) PG15 から public スキーマの CREATE が PUBLIC から外れた 専用スキーマを作る/必要なロールにだけ GRANT CREATE
6 permission denied for sequence items_item_id_seq serial のシーケンスに USAGE がない GRANT USAGE ON SEQUENCE または ON ALL SEQUENCES
7 permission denied for table tasks(INSERT ... RETURNING) RETURNING には SELECT 権限も必要 SELECT も付与
8 must be owner of table tasks ALTER・DROP は所有者だけ 所有者ロールで実行/所有者ロールのメンバーにする
9 permission denied for function secret_count 関数の EXECUTE が PUBLIC から外されている GRANT EXECUTE ON FUNCTION ...
10 permission denied for table tasks / CONTEXT: SQL function "task_count" 関数の中で使うテーブルの権限がない テーブル権限を付与/SECURITY DEFINER
11 permission denied for table tasks(SELECT *) 列単位の権限しかない 許可された列だけを指定して SELECT
12 permission denied for schema app(NOINHERIT ユーザー) NOINHERIT だとグループ権限を自動で使わない SET ROLE グループ してから実行/INHERIT にする

1. データベースに接続できない(CONNECT)

PGPASSWORD=change-me-4 psql -h 127.0.0.1 -U t88_guest -d t88 -c "select 1"
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL:  permission denied for database "t88"
DETAIL:  User does not have CONNECT privilege.

テンプレートで REVOKE ALL ON DATABASE t88 FROM PUBLIC をしているので、グループに入っていないロールは接続できません。 既定では PUBLIC に CONNECT があるため、どのロールでも接続できる状態から始まります。なお、pg_hba.conf で拒否された場合は no pg_hba.conf entry という別のエラーになります。

2. スキーマの USAGE がない

GRANT CONNECT ON DATABASE t88 TO t88_guest;
GRANT SELECT ON app.tasks TO t88_guest;     -- テーブルには付けた
SET ROLE t88_guest;
SELECT * FROM app.tasks;
ERROR:  permission denied for schema app
LINE 1: SELECT * FROM app.tasks;
                      ^
RESET ROLE;
GRANT USAGE ON SCHEMA app TO t88_guest;    -- 直した後
SET ROLE t88_guest;
SELECT count(*) FROM app.tasks;            -- → 1

エラーが「for table」ではなく「for schema」になっているのが見分けるポイントです。

3. 後から作ったテーブルが読めない

GRANT SELECT ON ALL TABLES IN SCHEMA app TO t88_guest;
SET ROLE t88_migration;
CREATE TABLE app.notes (note_id int PRIMARY KEY, body text);   -- GRANT の後に作成
RESET ROLE;
SET ROLE t88_guest;
SELECT * FROM app.notes;
ERROR:  permission denied for table notes

GRANT ... ON ALL TABLES IN SCHEMA は、その時点で存在するテーブルにだけ権限を付ける一度きりの操作です。 今後のテーブルにも自動で付けたい場合は ALTER DEFAULT PRIVILEGES を使います(テンプレートの手順6)。

4. ALTER DEFAULT PRIVILEGES を設定したのに効かない

SET ROLE t88_migrator;                        -- ログインユーザーのまま作成
CREATE TABLE app.made_by_login_user (id int);
RESET ROLE;
SET ROLE t88_app_user;
SELECT * FROM app.made_by_login_user;
ERROR:  permission denied for table made_by_login_user

既定権限は「誰が作ったか(FOR ROLE)」と「どのスキーマか(IN SCHEMA)」の両方が一致したときだけ適用されます。 \ddp の Owner 列と、\dt+ で見たテーブルの所有者が同じかを確認します。

確認すること 見る場所
既定権限の作成者・スキーマ \ddp の Owner・Schema
テーブルの実際の所有者 \dt+ app.* の Owner
直し方 DDL 前に SET ROLE t88_migration。既存分は ALTER TABLE ... OWNER TO t88_migration か個別に GRANT

5. PG15 以降、public スキーマに CREATE できない

SET ROLE t88_app_user;
CREATE TABLE public.tmp_work (id int);
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE public.tmp_work (id int);
                     ^
  Name  |       Owner       |           Access privileges            |      Description
--------+-------------------+----------------------------------------+------------------------
 public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
        |                   | =U/pg_database_owner                   |

PG15 から、public スキーマの CREATE 権限は PUBLIC(全ロール)から外され、所有者も pg_database_owner(そのDBの所有者)になりました。 =U/... は「全員に USAGE だけ」を表しています。PG14 以前から移行した環境ではこの変更が引き継がれない場合があるので、\dn+ public で確認してください。アプリ用のテーブルは、テンプレートのように専用スキーマを作って置くと権限を整理しやすくなります。

6. シーケンスの権限がない(serial)

CREATE ROLE t88_writer_noseq NOLOGIN;
GRANT USAGE ON SCHEMA app TO t88_writer_noseq;
GRANT INSERT ON app.items, app.tasks TO t88_writer_noseq;   -- シーケンスには付けない
SET ROLE t88_writer_noseq;
INSERT INTO app.items (name) VALUES ('ノート');               -- serial
INSERT INTO app.tasks (title) VALUES ('IDENTITY列は通る');    -- GENERATED ... AS IDENTITY
ERROR:  permission denied for sequence items_item_id_seq
INSERT 0 1

serial 列はシーケンスの USAGE 権限が必要ですが、GENERATED ALWAYS AS IDENTITY の列はテーブルの INSERT 権限だけで採番できました。 新しく作るテーブルは IDENTITY 列にしておくと、シーケンスの GRANT 漏れが起きません。

7. INSERT ... RETURNING で失敗する

SET ROLE t88_writer_noseq;
INSERT INTO app.tasks (title) VALUES ('returning') RETURNING task_id;
ERROR:  permission denied for table tasks

RETURNING は挿入した行を「読む」ので、SELECT 権限も必要です。 ORM は内部で RETURNING を使うことが多いため、書き込み用ロールには SELECT も付けておきます。UPDATE ... WHERE や DELETE ... WHERE も、条件の列を読むため SELECT が必要です。

8. 所有者でないと ALTER・DROP できない

SET ROLE t88_app_user;
ALTER TABLE app.tasks ADD COLUMN due date;
TRUNCATE app.tasks;
ERROR:  must be owner of table tasks
ERROR:  permission denied for table tasks

ALTER TABLE や DROP TABLE は「must be owner」、TRUNCATE は TRUNCATE 権限がないため「permission denied」と、エラー文が異なります。 スキーマ変更はマイグレーション用ロールで行い、アプリ用ロールには付けない設計が扱いやすくなります。

9・10. 関数の EXECUTE と、関数の中のテーブル権限

SET ROLE t88_migration;
CREATE FUNCTION app.secret_count() RETURNS bigint LANGUAGE sql AS 'SELECT count(*) FROM app.tasks';
REVOKE EXECUTE ON FUNCTION app.secret_count() FROM PUBLIC;
RESET ROLE;
SET ROLE t88_guest;
SELECT app.secret_count();
ERROR:  permission denied for function secret_count

関数の EXECUTE は既定で PUBLIC に付いているため、通常は呼び出せます。 外したい関数は REVOKE EXECUTE ... FROM PUBLIC してから必要なロールにだけ付与します(ALTER DEFAULT PRIVILEGES ... REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC で以後の関数にも適用可能)。

関数を呼べても、中で使うテーブルの権限は「呼び出したロール」でチェックされます。

GRANT EXECUTE ON FUNCTION app.task_count() TO t88_writer_noseq;
SET ROLE t88_writer_noseq;      -- tasks の SELECT 権限なし
SELECT app.task_count();
ERROR:  permission denied for table tasks
CONTEXT:  SQL function "task_count" statement 1
RESET ROLE;
ALTER FUNCTION app.task_count() SECURITY DEFINER SET search_path = pg_catalog, app;
SET ROLE t88_writer_noseq;
SELECT app.task_count();        -- → 2

SECURITY DEFINER にすると、関数の所有者の権限で中身が実行されます。 その場合は SET search_path を必ず固定し、EXECUTE を付けるロールを絞るのが安全な使い方です。

11. 列単位の権限で SELECT * をした

CREATE ROLE t88_colonly NOLOGIN;
GRANT USAGE ON SCHEMA app TO t88_colonly;
GRANT SELECT (task_id, title) ON app.tasks TO t88_colonly;
SET ROLE t88_colonly;
SELECT * FROM app.tasks;
SELECT task_id, title FROM app.tasks;
ERROR:  permission denied for table tasks
 task_id |      title
---------+------------------
       1 | 備品を発注する
       2 | IDENTITY列は通る

\dp では「Column privileges」列に表示されます。

 Name  |  ...  |       Column privileges
-------+-------+-------------------------------
 tasks |  ...  | task_id:                     +
       |       |   t88_colonly=r/t88_migration+
       |       | title:                       +
       |       |   t88_colonly=r/t88_migration

12. NOINHERIT のユーザー

CREATE ROLE t88_noinherit LOGIN NOINHERIT IN ROLE t88_readonly;
SET ROLE t88_noinherit;
SELECT count(*) FROM app.tasks;     -- 失敗
SET ROLE t88_readonly;
SELECT count(*) FROM app.tasks;     -- → 2
ERROR:  permission denied for schema app

NOINHERIT のロールは、所属グループの権限を SET ROLE で切り替えたときだけ使えます。 普段は弱い権限で作業し、必要な時だけ強い権限に切り替える運用に使います。

権限を一括で確認するSQL

SELECT r AS role,
  has_database_privilege(r, 't88', 'CONNECT') AS db_connect,
  has_schema_privilege(r, 'app', 'USAGE')     AS schema_usage,
  has_table_privilege(r, 'app.tasks', 'SELECT') AS tbl_select,
  has_table_privilege(r, 'app.tasks', 'INSERT') AS tbl_insert,
  has_sequence_privilege(r, 'app.items_item_id_seq', 'USAGE') AS seq_usage
FROM unnest(ARRAY['t88_report_user','t88_app_user','t88_migrator','t88_guest']) AS r;
      role       | db_connect | schema_usage | tbl_select | tbl_insert | seq_usage
-----------------+------------+--------------+------------+------------+-----------
 t88_report_user | t          | t            | t          | f          | f
 t88_app_user    | t          | t            | t          | t          | t
 t88_migrator    | t          | t            | t          | t          | t
 t88_guest       | f          | t            | t          | f          | f

has_*_privilege() は所属グループ経由の権限も含めて判定するので、「このユーザーは結局何ができるのか」を一覧で確認できます。 permission denied の調査では、まずこの表で階層のどこが f かを見ると早く原因に届きます。

PG16のロール付与オプション(INHERIT / SET)

PG16 から、GRANT ロール TO ユーザー に INHERIT と SET のオプションを個別に指定できるようになりました。 \drg の Options 列に表示されていたのがこれです。

オプション 意味
INHERIT TRUE グループの権限を自動で使える
SET TRUE SET ROLE グループ で切り替えられる(作るオブジェクトの所有者をグループにできる)
ADMIN TRUE 他のロールをそのグループに追加・削除できる
CREATE ROLE t88_batch LOGIN PASSWORD 'change-me-6';
GRANT t88_readonly TO t88_batch WITH INHERIT TRUE, SET FALSE;
 Role name |  Member of   | Options | Grantor
-----------+--------------+---------+----------
 t88_batch | t88_readonly | INHERIT | postgres
PGPASSWORD=change-me-6 psql -h 127.0.0.1 -U t88_batch -d t88 \
  -c "select count(*) from app.tasks" -c "set role t88_readonly"
 count
-------
     2
(1 row)

ERROR:  permission denied to set role "t88_readonly"

「権限は使えるが、そのロールになりすますことはできない」設定にできます。 なお SET ROLE が許されるかはログインしたユーザー(session_user)で判定されるため、この確認は実際にログインして行っています。PG16 では CREATEROLE 属性を持つロールができることも整理され、自分が作ったロールなど ADMIN を持つロールだけを管理できるようになっています。

定義済みロールで権限をまとめて渡す

定義済みロール(predefined roles)は、よく使う権限のセットをあらかじめ用意したロールです。 GRANT pg_read_all_data TO ユーザー; のように付与します。PG18 の公式ドキュメントに載っている主なものは次のとおりです。

ロール できること 追加された版
pg_read_all_data 全テーブル・ビュー・シーケンスを SELECT できる(スキーマ USAGE も含む) PG14
pg_write_all_data 全テーブルに INSERT・UPDATE・DELETE できる PG14
pg_database_owner 暗黙のメンバーがDBの所有者。public スキーマの所有者 PG14
pg_monitor 監視用ビュー・関数(pg_read_all_settings・pg_read_all_stats・pg_stat_scan_tables を含む) PG10
pg_signal_backend 他のセッションのキャンセル・終了 PG9.6
pg_checkpoint CHECKPOINT の実行 PG15
pg_create_subscription CREATE SUBSCRIPTION の実行(DBの CREATE 権限も必要) PG16
pg_use_reserved_connections reserved_connections で予約した接続枠を使える PG16
pg_maintain 全テーブルに VACUUM・ANALYZE・REINDEX・CLUSTER・LOCK TABLE など PG17
pg_signal_autovacuum_worker autovacuum ワーカーへのキャンセル・終了シグナル PG18
pg_read_server_files 等 サーバー上のファイル読み書き・プログラム実行 PG11

PG16 で pg_read_all_data を確かめると、個別の GRANT がないテーブルも読めましたが、書き込みはできませんでした。

CREATE ROLE t88_auditor LOGIN PASSWORD 'change-me-5';
GRANT CONNECT ON DATABASE t88 TO t88_auditor;
GRANT pg_read_all_data TO t88_auditor;
SET ROLE t88_auditor;
SELECT count(*) FROM app.made_by_login_user;   -- 個別 GRANT なし → 読める
INSERT INTO app.notes VALUES (1,'x');
 count
-------
     0
(1 row)

ERROR:  permission denied for table notes

pg_read_all_data は便利ですが、データベースの CONNECT は別に必要で、後述の行レベルセキュリティ(RLS)も適用されます。

行レベルセキュリティ(RLS)の最小例

RLS は、テーブルの「どの行」を見られるかをポリシーで制御する機能です。 GRANT はテーブル単位・列単位までなので、「自分のタスクだけ見せる」には RLS を使います。

SET ROLE t88_migration;
CREATE TABLE app.todos (todo_id int PRIMARY KEY, owner_name text NOT NULL, title text);
INSERT INTO app.todos VALUES (1,'t88_app_user','自分のタスク'),(2,'someone_else','他人のタスク');
ALTER TABLE app.todos ENABLE ROW LEVEL SECURITY;
CREATE POLICY todos_owner ON app.todos
  USING (owner_name = current_user)          -- 読める行
  WITH CHECK (owner_name = current_user);    -- 書き込める行
RESET ROLE;

SET ROLE t88_app_user;
SELECT * FROM app.todos;
INSERT INTO app.todos VALUES (3,'someone_else','なりすまし');
 todo_id |  owner_name  |    title
---------+--------------+--------------
       1 | t88_app_user | 自分のタスク
(1 row)

ERROR:  new row violates row-level security policy for table "todos"
ロール 見えた行数 理由
t88_app_user 1 ポリシーで自分の行だけ
t88_migration(所有者) 2 所有者には RLS が適用されない(FORCE ROW LEVEL SECURITY で適用可)
t88_auditor(pg_read_all_data) 0 定義済みロールでもポリシーは適用される

監査用に全行を見せたいロールには、BYPASSRLS 属性を付けるか、そのロール用のポリシーを追加します。 アプリが1つのDBユーザーで接続する場合は、current_user の代わりに current_setting('app.member_id') のようなセッション変数を条件に使う方法が一般的です。

REVOKE と DROP ROLE の手順

REVOKE は「付けた経路」から外す

REVOKE SELECT ON app.tasks FROM t88_report_user;
SET ROLE t88_report_user;
SELECT count(*) FROM app.tasks;    -- → 2(まだ読める)

個人に直接付いていない権限は、個人から REVOKE しても消えません。 t88_report_user はグループ t88_readonly 経由で SELECT を持っているため、外すなら REVOKE t88_readonly FROM t88_report_user; でグループから抜けます。

外したいもの SQL
グループからの所属 REVOKE t88_readonly FROM t88_report_user;
テーブル権限 REVOKE SELECT ON app.tasks FROM role;
既定権限 ALTER DEFAULT PRIVILEGES FOR ROLE 作成者 IN SCHEMA app REVOKE SELECT ON TABLES FROM role;
PUBLIC の既定 REVOKE ALL ON DATABASE db FROM PUBLIC; / REVOKE EXECUTE ON FUNCTION f() FROM PUBLIC;

DROP ROLE は REASSIGN OWNED → DROP OWNED → DROP ROLE

DROP ROLE t88_migration;
ERROR:  role "t88_migration" cannot be dropped because some objects depend on it
DETAIL:  privileges for database t88
owner of schema app
owner of default privileges on new relations belonging to role t88_migration in schema app
owner of default privileges on new sequences belonging to role t88_migration in schema app
owner of default privileges on new functions belonging to role t88_migration in schema app
owner of sequence app.tasks_task_id_seq
owner of table app.tasks
...

ロールが何かを所有していたり、どこかに権限を持っていたりすると削除できません。 次の順で片付けます。

-- 1) 所有物を別のロールへ移す(DB ごとに実行)
REASSIGN OWNED BY t88_migrator TO t88_migration;
-- 2) 残った権限・既定権限・所有物を削除(DB ごとに実行)
DROP OWNED BY t88_guest;
-- 3) ロールを削除
DROP ROLE t88_guest;
REASSIGN OWNED
DROP OWNED
DROP ROLE

REASSIGN OWNED と DROP OWNED は、実行したデータベースの中だけに効きます。 複数のDBにオブジェクトがある場合は、それぞれのDBに接続して実行してから DROP ROLE します。DROP OWNED は所有するテーブルも削除するので、必ず REASSIGN OWNED を先に行ってください。

よくある質問

Q. 読み取り専用ユーザーを最短で作るには? A. PG14 以降なら CREATE ROLE reader LOGIN PASSWORD '...'; GRANT CONNECT ON DATABASE appdb TO reader; GRANT pg_read_all_data TO reader; が最短です。スキーマを限定したい場合は、本記事のテンプレートのように USAGE・SELECT・ALTER DEFAULT PRIVILEGES を組み合わせます。

Q. ALTER DEFAULT PRIVILEGES は既存のテーブルにも効きますか? A. 効きません。設定後に作られるオブジェクトだけが対象です。既存分は GRANT ... ON ALL TABLES IN SCHEMA で付け、今後の分は既定権限で付ける、の2本立てにします。

Q. スーパーユーザーで作ったテーブルがアプリから見えません。 A. 所有者がスーパーユーザーになっているため、FOR ROLE t88_migration の既定権限が適用されていません。ALTER TABLE app.xxx OWNER TO t88_migration; で所有者をそろえ、必要な権限を GRANT し直してください。

Q. マネージドサービス(RDS など)でも同じ設計で使えますか? A. GRANT・ロール・既定権限の仕組みは同じです。ただし本当のスーパーユーザーは使えず、サービスごとの管理用ロール(例:rds_superuser)が用意されているので、ロール属性や定義済みロールの付与範囲は各サービスのドキュメントで確認してください(本記事の検証はセルフホストの PostgreSQL 16 で行っています)。

まとめ

  • 権限は「DB の CONNECT → スキーマの USAGE → テーブルの SELECT 等」の階層で、1段でも欠けると permission denied になる
  • NOLOGIN のグループ(readonly / readwrite / migration)に権限を集め、ログインユーザーは所属させるだけにする
  • 既存は GRANT ON ALL TABLES、今後は ALTER DEFAULT PRIVILEGES。既定権限は作成者が FOR ROLE と一致したときだけ効くので、DDL は SET ROLE してから行う
  • PG15 から public スキーマに一般ロールは CREATE できない。アプリ用には専用スキーマを使う
  • serial はシーケンス権限、RETURNING は SELECT 権限、ALTER は所有者が必要。IDENTITY 列ならシーケンスの GRANT は不要
  • PG16 の \drg と GRANT の INHERIT / SET オプション、PG17 の pg_maintain、PG18 の pg_signal_autovacuum_worker で、より細かく権限を分けられる
  • ロール削除は REASSIGN OWNED → DROP OWNED → DROP ROLE の順で、DB ごとに行う

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQLのロールと権限|GRANT設計テンプレートとエラー対処」

株式会社RJCでは、PostgreSQLの権限設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?