本文へスキップ / Skip to main content
KINTO Tech Blog

約20分で読めます

DBRE

PostgreSQLのCREATEDBはGRANTでは付けられない ―― MySQLとの権限モデルの違いと、SET ROLE利用時の落とし穴

PostgreSQLのCREATEDBはGRANTでは付けられない ―― MySQLとの権限モデルの違いと、SET ROLE利用時の落とし穴 cover

こんにちは。KINTO テクノロジーズの DBRE チーム所属の makoto.sato です。2026年3月に中途入社し、普段は Aurora(MySQL / PostgreSQL)の運用・信頼性向上や、社内向け DB 作業基盤の開発をしています。前職では長く DBA としてオンプレの MySQL / PostgreSQL / Oracle を運用していました。

はじめに

KINTO テクノロジーズの DBRE チームでは、DB 作業の申請〜承認〜一時的な作業環境の自動構築までを Slack から完結させる社内ツール「PowerPole」を開発・運用しています。全体像は Amazon Web Services ブログの クルマのサブスク「KINTO」のアジリティとガバナンスを両立した DBRE の取り組み で紹介しました。

仕組みをかいつまんで言うと、Slack から申請 → 承認 → EC2 と一時的な DB 認証情報が自動発行 → 作業完了後に自動削除、という流れで、踏み台構築から利用終了までのガバナンスと作業スピードを両立させるものです。

PowerPole はこれまで Aurora MySQL のみ対応でしたが、社内での Aurora PostgreSQL 利用の広がりに合わせて PostgreSQL 対応を進めることになりました。本記事では、その中で遭遇した「権限を付けたはずなのに効かない」問題と、その根底にあった MySQL と PostgreSQL の権限モデルの違いについて書きます。

なお、扱う挙動はすべてコミュニティ版 MySQL / PostgreSQL 共通の仕様で、Aurora 固有のものではありません。検証環境は Aurora PostgreSQL 17 と MySQL 8.0 です。MySQL のロール機能は 8.0 で追加されたものなので、5.7 では本記事の MySQL 側の記述は成立しません。CREATEROLE は PostgreSQL 16 で意味が大きく変わっているため、15 以前の環境では本記事の構成をそのまま適用しないでください。

症状:\du には権限が見えているのに、権限エラーになる

あるロールに CREATEDB を付与し、\du(psql でロールの一覧と属性を表示するコマンド)でも確かに付いていることを確認しました。

                  List of roles
 Role name |       Attributes
-----------+-------------------------
 dbuser    | Create role, Create DB

ところが、その dbuser で接続してデータベースを作ろうとすると失敗します。

CREATE DATABASE test_db;
-- ERROR:  permission denied to create database

属性は付いている。しかし効いていない。一見矛盾した状況です。

MySQL 版ではどう書いていたか

PowerPole には、申請時に選べる権限レベルの 1 つとして「DDLOperator」があります。テーブルやデータベースの作成・変更を一時的に許可する権限レベルです。

MySQL 版では、pp_ddl_operator という共有ロールに必要な権限をすべて集約し、発行する一時ユーザーにこのロールを付与しています(一部抜粋)。

CREATE ROLE 'pp_ddl_operator';
GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX,
      CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES
      ON *.* TO 'pp_ddl_operator';
GRANT CREATE USER ON *.* TO 'pp_ddl_operator';   -- ユーザーとロールの作成・削除
GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator';    -- ロールの付与・剥奪

-- 一時ユーザーへロールを付与し、次回ログイン時に自動で有効になるよう既定ロールにする
-- (MySQL は activate_all_roles_on_login が既定 OFF なので、付与しただけでは有効にならない)
GRANT 'pp_ddl_operator' TO 'dbuser';
ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator';

ここで注目してほしいのは、権限の種類が違っても書き方が変わらない点です。テーブル操作のようなオブジェクト権限も、DB 作成やユーザー作成のようなサーバー全体に関わる能力も、すべて同じ GRANT ... ON *.* で表現できています。

PostgreSQL 版でも同じ設計を踏襲し、共有ロールに権限を集約して一時ユーザー dbuser にロールを付与する構成にしました。テーブルやスキーマへのオブジェクト権限は GRANT で集約でき、ここまでは順調でした。

つまずき①:GRANT CREATEDB という書き方はできない

問題は「データベース作成」「ロール作成」でした。MySQL の感覚でこう書きたくなりますが、エラーになります。

GRANT CREATEDB TO pp_ddl_operator;
-- ERROR:  role "createdb" does not exist

PostgreSQL の GRANT <名前> TO <ロール> はロールメンバーシップの付与として解釈されます。PostgreSQL は「createdb という名前のロール」を探し、存在しないためエラーになります。

さらに紛らわしいのが次の書き方です。構文としては通りますが、意味はまったく違います。

GRANT CREATE ON DATABASE mydb TO pp_ddl_operator;  -- 通るが「DB作成権限」ではない

これは「mydb の中にスキーマなどを作る権限」であって、データベースを作る権限ではありません。

データベース作成・ロール作成は、GRANT とは別の「ロール属性」という仕組みで表現されていて、ALTER ROLE(または CREATE ROLE 時のオプション)でしか付与できません。

ALTER ROLE <ロール名> CREATEDB CREATEROLE;

つまずき②:属性を付けたのに効かない

ALTER ROLE で付ければよいと分かったので、次はどのロールに付けるかです。最初は一時ユーザーの dbuser に付与しました。

ALTER ROLE dbuser CREATEDB CREATEROLE;

dbuser は一定期間を過ぎると削除される使い捨てのユーザーなので、属性もユーザーごと消えます。常設の共有ロールに永続的な属性を持たせるより影響範囲が狭く、妥当な選択に思えました。

その結果が、冒頭に書いた「\du には見えているのに権限エラー」でした。

原因調査

PowerPole が生成しているユーザー作成処理では、このように権限を付与しています。

GRANT pp_ddl_operator TO dbuser;
ALTER ROLE dbuser SET role = 'pp_ddl_operator';   -- role パラメータの値として渡すのでクォートしている(識別子でも書ける)

2 行目は role パラメータの既定値をロール単位で設定するもので、dbuser でログインするとセッション開始時に自動的に SET ROLE pp_ddl_operator された状態になります。利用者が接続後に毎回 SET ROLE を打たなくても、付与されたロールの権限ですぐ作業を始められるように入れているものです。

この SET ROLE が怪しいので、実際に接続して確認してみます。

SELECT current_user, session_user;
--   current_user    | session_user
-- ------------------+--------------
--  pp_ddl_operator  | dbuser

session_user(ログインしたロール)は dbuser のままですが、current_user(権限チェックの対象になるロール)は pp_ddl_operator に切り替わっていました。

PostgreSQL のロール属性チェックは、current_user そのものの属性だけを見ます。(CREATEDB / CREATEROLE / SUPERUSER のように SQL の実行時に評価される属性の話です。LOGIN は接続を確立する時点でログインロールに対して評価されるので、SET ROLE の影響を受けません。)CREATE DATABASE 実行時に評価されるのは pp_ddl_operator の属性であって、dbuser の属性ではありません。dbuser にいくら CREATEDB を付与しても、既定の current_user では評価対象になっていなかったのです。

メンバーシップで継承されるのはオブジェクト権限だけ

「dbuser は pp_ddl_operator のメンバーなのだから、属性もどこかで繋がるのでは?」と思うかもしれません。しかし、継承されるのはオブジェクト権限(テーブルの SELECT / INSERT など)に限られ、CREATEDB / CREATEROLE / LOGIN / SUPERUSER といったロール属性は継承対象外です。これはドキュメントにも明記されています。

The role attributes LOGIN, SUPERUSER, CREATEDB, and CREATEROLE can be thought of as special privileges, but they are never inherited as ordinary privileges on database objects are. You must actually SET ROLE to a specific role having one of these attributes in order to make use of the attribute.

— PostgreSQL Documentation: Role Membership

つまり、同じ「権限」と呼ばれるものの中に、評価のされ方が違う 2 種類が同居しています。

評価の対象 継承
ロール属性(CREATEDB など) current_user そのものの属性だけ されない
オブジェクト権限(テーブルの SELECT など) current_user と、それが継承しているロール群 される

図にすると次のようになります。

CREATE DATABASE の権限判定で参照されるのは current_user であり、session_user の属性もメンバーシップ経由の属性も使われない

\du で属性が見えていたのに効かなかった理由は、この 2 つの仕様の組み合わせでした。

  1. dbuser の属性は、SET ROLE 後の current_user(pp_ddl_operator)の評価では参照されない
  2. メンバーシップがあっても、属性は継承されない

この非対称性は、Aurora / RDS を使っていれば身近なところに現れています。マスターユーザーは rds_superuser のメンバーですが、その定義は AWS のドキュメントで次のように示されています。

CREATE ROLE postgres WITH LOGIN NOSUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION VALID UNTIL 'infinity'

— Amazon Aurora ユーザーガイド: rds_superuser ロールを理解する

CREATEDB と CREATEROLE がマスターユーザー自身に直接付与されています。rds_superuser のメンバーであることで属性が得られるなら、この直接付与は要りませんでした。

対処

原因は「既定の current_user が共有ロールになっていること」なので、属性を付ける先を、実際に current_user として振る舞うロールに変えます。

ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE;

これで期待どおりデータベース・ロールの作成ができるようになりました。結果として、MySQL 版と同じ「権限はすべて共有ロールに集約し、ユーザーにはロールを渡すだけ」という形に属性の置き場所も揃いました。

共有ロールに属性が残り続けることが気になるかもしれませんが、pp_ddl_operator は NOLOGIN で直接ログインできず、属性を使えるのはメンバーシップを持つロールに限られます。付与と剥奪は申請のライフサイクルで管理されています。逆に言えば実際の境界は NOLOGIN ではなくメンバーシップなので、恒久ユーザーにこのロールを付与しないことが前提になります。

なぜ GRANT に乗らないのか ―― オブジェクト空間の違い

ここまでが実際にハマった話です。最後に、そもそもなぜ CREATEDB が GRANT で書けないのかを整理します。

MySQL では Database と Schema は同義で、サーバーの中はフラットな構造です。

Server(MySQL サーバー全体)← GRANT ... ON *.* がここ全体を覆う
  └── Database(= Schema。同義)
        └── Table / View / Procedure / ...

DB を作ることも、その中にテーブルを作ることも同じ空間の中の出来事なので、この記事で扱う権限セットはすべて GRANT ... ON *.* の 1 つの語彙で書けます。

一方 PostgreSQL は 3 層構造です。

Cluster(PostgreSQL サーバー全体)← Database作成・Role作成はここ(ロール属性で管理)
  └── Database(完全に分離された名前空間)
        └── Schema(Database 内の名前空間)
              └── Table / View / Function / ...

GRANT で付与するオブジェクト権限は、Database や Schema といった入れ物ごとに指定する形になります。MySQL の ON *.* に相当する全 Database 一括指定が無いのは、テーブルなどのカタログが Database ごとに分離していて、接続中の Database の外にあるオブジェクトを指定できないためです。

では Database や Role はどうかというと、これらはクラスタ全体で共有されるカタログに属します。ただし層が違うから GRANT に乗らない、という単純な話ではありません。GRANT ... ON DATABASE や GRANT ... ON TABLESPACE は、クラスタ全体に関わる対象を GRANT で扱えています。

違いは権限を書き留める先があるかどうかです。GRANT は権限を対象オブジェクトのアクセス制御リスト(ACL)に書き込みますが、CREATE DATABASE と CREATE ROLE が相手にするクラスタ自身は、ACL を持つオブジェクトとして存在しません。そのため PostgreSQL はこれらをロール自身の属性として保持し、ALTER ROLE で操作します。ただし定義済みロールという抜け道はあり、PostgreSQL 16 の pg_create_subscription は「まだ存在しないものを作る能力」をメンバーシップで配っています。CREATEDB / CREATEROLE が属性側にあるのは原理的な制約ではなく、仕組みが古いという経緯です。

いずれにしても、MySQL で 1 つの GRANT に乗っていた権限セットは、PostgreSQL では「オブジェクトの ACL に書く権限」と「ロール自身に持たせる属性」の 2 系統に分かれます。

同じ権限セットを書き比べると

DDLOperator 相当の権限セット(DDL/DML + DB 作成 + ロール作成)を共有ロールに集約すると、こうなります。

MySQL はすべて GRANT で完結します。

CREATE ROLE 'pp_ddl_operator';

-- オブジェクト権限もサーバーレベルの能力も、すべて同じ GRANT
GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX,
      CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES
      ON *.* TO 'pp_ddl_operator';
GRANT CREATE USER ON *.* TO 'pp_ddl_operator';   -- ユーザーとロールの作成も GRANT
GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator';    -- ロールの付与・剥奪も GRANT

-- ユーザーへはロールを付与し、次回ログイン時に有効になるよう既定ロールにする
GRANT 'pp_ddl_operator' TO 'dbuser';
ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator';

PostgreSQL は 2 系統に分かれます。

CREATE ROLE pp_ddl_operator NOLOGIN;

-- オブジェクト権限は GRANT。ただし *.* のような全体指定は無く、
-- データベース・スキーマごとに付与する(ALL TABLES は実行時点の既存テーブルのみ)
GRANT ALL PRIVILEGES ON SCHEMA myschema TO pp_ddl_operator;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA myschema TO pp_ddl_operator;
GRANT TEMPORARY ON DATABASE mydb TO pp_ddl_operator;

-- データベース作成・ロール作成は GRANT では書けない。ALTER ROLE で属性として付与する
ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE;

-- ユーザーへはメンバーシップを付与
GRANT pp_ddl_operator TO dbuser;

先頭の CREATE ROLE を除くと、MySQL 側は最後の 1 行以外すべて GRANT で、PostgreSQL 側は ALTER ROLE の 1 行だけが別系統に落ちます。この 1 行が今回のハマりどころでした。

やりたいことを軸に並べると、対応はこうなります。

やりたいこと MySQL PostgreSQL
テーブルの DML GRANT ... ON *.* GRANT ... ON ALL TABLES IN SCHEMA x(既存テーブルのみ。以後の分は ALTER DEFAULT PRIVILEGES)
テーブルの ALTER / DROP GRANT ALTER, DROP ON *.* GRANT では不可。所有ロールのメンバーになる
一時テーブル GRANT CREATE TEMPORARY TABLES ON *.* GRANT TEMPORARY ON DATABASE x
データベース作成 GRANT CREATE ON *.* ALTER ROLE x CREATEDB
ロール・ユーザー作成 GRANT CREATE USER ON *.* ALTER ROLE x CREATEROLE
ロールの付与 GRANT 'role' TO 'user' GRANT role TO user
ロールの有効化 ALTER USER 'user' DEFAULT ROLE 'role' ALTER ROLE user SET role = 'role'(任意)

MySQL 側は GRANT 主体で埋まり、PostgreSQL 側はデータベース作成とロール作成の 2 行だけ ALTER ROLE に落ちます。

もう 1 つ、GRANT に乗らないものは CREATEDB / CREATEROLE だけではありません。テーブルの ALTER / DROP は PostgreSQL では所有権に固有の権利で、GRANT では切り出せません。MySQL の ALTER / DROP 権限に相当するものが無いので、DDL を打たせたい場合は対象の所有ロールに参加させる形になります。所有者かどうかの判定は所有ロールのメンバーであれば満たされるので、ここは属性と違って共有ロール経由で渡せます。

MySQL 側も実は 2 系統ある

MySQL は「すべて GRANT で書ける」ように見えますが、これは構文の見せ方の話です。

MySQL でも CREATE USER や ROLE_ADMIN のようなグローバル権限は、オブジェクトの ACL ではなくアカウントに紐づいて格納されています(静的権限は mysql.user の列、ROLE_ADMIN のような動的権限は mysql.global_grants の行として)。この点は PostgreSQL のロール属性と本質的に同じです。さらに次のものは GRANT では書けず、CREATE USER / ALTER USER で指定します。

  • ACCOUNT LOCK / UNLOCK
  • PASSWORD EXPIRE などのパスワードポリシー
  • MAX_QUERIES_PER_HOUR などのリソース制限
  • REQUIRE SSL、認証プラグインの指定
  • DEFAULT ROLE(SET DEFAULT ROLE でも設定できる)

なお SSL 要件・リソース制限・認証方式は 5.7 までは GRANT でも書けましたが、5.7 で非推奨になり 8.0.11 で削除されました。

つまり 2 系統に分かれているのは PostgreSQL だけではありません。違いは系統の数ではなく、「データベースを作る」「ロールを作る」という能力がどちら側に置かれているかです。

SET ROLE の意味論も違う

もう 1 つ、MySQL にも SET ROLE はありますが意味論が違います。

MySQL PostgreSQL
ロール有効化後の現在のユーザー 変わらない(有効ロールは CURRENT_ROLE() で確認) 切り替わる(current_user が置き換わる)
権限の合成 アカウント自身の権限と有効ロールの権限の和集合 current_user 基準。ロール属性は継承されない

MySQL は加算的なので、「ユーザー側に付けた権限がロール有効化で見えなくなる」という事故は起きません。「SET ROLE があるかどうか」ではなく「SET ROLE が current_user を置き換えるかどうか」 が、今回の落とし穴の分かれ目でした。

CREATEROLE はバージョンで危険度が違う

PostgreSQL 15 以前では、CREATEROLE を持つロールが他の非スーパーユーザーのパスワードを変更でき、定義済みロールを自分に付与できました。つまり CREATEROLE はスーパーユーザーに準じる強さを持っていました(既存のスーパーユーザーには手を出せません)。

PostgreSQL 16 でこの挙動は変更され、権限の範囲が自分が管理するロールに限定されています。本記事の構成は 16 以降を前提としているので、15 以前の環境で共有ロールに CREATEROLE を付与するのは避けてください。

まとめ

  • PostgreSQL の CREATEDB / CREATEROLE は GRANT では書けず、ALTER ROLE で付与する「ロール属性」
  • ロール属性は current_user そのものだけを見て、継承されない。一方オブジェクト権限は current_user と継承ロール群を見る。この非対称性が落とし穴の正体
  • したがって SET ROLE を使う構成では、属性の付与先は current_user を基準に決める

移植のチェックポイントとして一般化するなら、MySQL で ON *.* と書いていた GRANT 文を洗い出し、どれがオブジェクト権限でどれがロール属性かを最初に仕分けることです。そして権限が効かないときに最初に打つべきは SELECT current_user, session_user; です。この 1 行で切り分けられる問題は、思っているより多いはずです。

参考

Facebook

Follow Us

XConnpassWantedly

KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。