RDS (PostgreSQL) でデータベースを作成するときの注意点

RDS(PostgreSQL)でデータベース作成をしようとしてハマったのでメモ。

# 新規ユーザが所有者のデータベースを作りたい

マスタユーザでログインして、

* ユーザ（ロール）作成
* 上で作成したロールがOWNERなデータベース作成

をしようとすると、以下のようなエラーが出る。

```
rdsdb=> CREATE ROLE test WITH PASSWORD 'testpass' LOGIN;
CREATE ROLE
rdsdb=> CREATE DATABASE testdb WITH OWNER test;
ERROR:  must be member of role "test"
```

## 対処法

エラーを回避するためには、マスタユーザが新たに作成したロールのメンバになっていないといけない。
つまり、`GRANT`文で

```
rdsdb=> GRANT test TO bwtakacyaws;
GRANT ROLE
```
としてやればよい。ここで、testが新たに所有者となるロールで、bwtakacyawsがマスタユーザである。

```
rdsdb=> CREATE DATABASE testdb WITH OWNER test;
CREATE DATABASE
rdsdb=> \l
                                      List of databases
   Name    |    Owner    | Encoding |   Collate   |    Ctype    |      Access privileges      
-----------+-------------+----------+-------------+-------------+-----------------------------
 postgres  | bwtakacyaws | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
 rdsadmin  | rdsadmin    | UTF8     | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin
 rdsdb     | bwtakacyaws | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
 template0 | rdsadmin    | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin                +
           |             |          |             |             | rdsadmin=CTc/rdsadmin
 template1 | bwtakacyaws | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/bwtakacyaws             +
           |             |          |             |             | bwtakacyaws=CTc/bwtakacyaws
 testdb    | test        | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
(6 rows)
```

出来た！

## 原因

この事象自体はRDS固有ではなく、PostgreSQL自体の制約である。
が、RDSでスーパユーザの代わりに利用するマスタユーザだと必ず発生するので、エラーメッセージでググるといかにもRDS固有の制約のように見えがち。

http://stackoverflow.com/questions/26684643/error-must-be-member-of-role-when-creating-schema-in-postgresql

PostgreSQLドキュメントにもこの制約がきちんと書かれている。

> user_name
新しいデータベースを所有するユーザのロール名です。 デフォルト設定（つまり、コマンドを実行したユーザ）を使用する場合はDEFAULTと指定します。 他のロールによって所有されるデータベースを作成するためには、そのロールの直接的または間接的なメンバであるか、スーパーユーザでなければなりません。

[PostgreSQLドキュメント　CREATE DATABASE](https://www.postgresql.jp/document/9.5/html/sql-createdatabase.html)


実際、RDS上でマスタユーザの権限を確認すると、

```
rdsdb=> \du
                                                    List of roles
     Role name     |                         Attributes                         |              Member of              
-------------------+------------------------------------------------------------+-------------------------------------
 bwtakacyaws       | Create role, Create DB                                    +| {rds_superuser}
                   | Password valid until infinity                              | 
 pg_signal_backend | Cannot login                                               | {}
 rds_replication   | Cannot login                                               | {}
 rds_superuser     | Cannot login                                               | {rds_replication,pg_signal_backend}
 rdsadmin          | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                   | Password valid until infinity                              | 
 rdsrepladmin      | No inheritance, Cannot login, Replication                  | {}
```

となっていて、
**マスタユーザはCREATE ROLE, CREATE DB権限は持っているが、スーパユーザではない**

ローカルやオンプレのPostgreSQLだとこういったDB作成作業をスーパユーザですることも多いので、この制約に今まで気がつかなかった。

手元でインストールしたPostgreSQLでも同じ事象は起こる。

スーパユーザでログインして、RDSのマスタユーザ相当のユーザ`manager`を作って、

```
postgres=# CREATE ROLE manager NOSUPERUSER CREATEDB CREATEROLE LOGIN;
CREATE ROLE
postgres=> \du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 bwtakacy  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 django    | Create DB                                                  | {}
 luigi     |                                                            | {}
 manager   | Create role, Create DB   
```

`manager`でログインし直して、新規ロール作成とそのロールが所有する新規データベースを作成しようとすると同じエラーが出る。

```
$ psql -d postgres -U manager
postgres=> CREATE ROLE test;
CREATE ROLE
postgres=> CREATE DATABASE testdb WITH OWNER test;
ERROR:  must be member of role "test"
```

ちなみに、このような制約がある理由はセキュリティのためらしい。

PostgreSQLのソースコードを読むと、CREATE DATABASEを実行しているコードにコメントがあった。

```c:src/backend/commands/dbcommands.c
    /*   
     * To create a database, must have createdb privilege and must be able to
     * become the target role (this does not imply that the target role itself
     * must have createdb privilege).  The latter provision guards against
     * "giveaway" attacks.  Note that a superuser will always have both of
     * these privileges a fortiori.
     */
    if (!have_createdb_privilege())
        ereport(ERROR,
                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
                 errmsg("permission denied to create database")));

    check_is_member_of_role(GetUserId(), datdba);
```

check_is_member_of_roleという関数が件のエラーを出す。

```c:src/backend/utils/adt/acl.c
/*
 * check_is_member_of_role
 *      is_member_of_role with a standard permission-violation error if not
 */
void
check_is_member_of_role(Oid member, Oid role)
{
    if (!is_member_of_role(member, role))
        ereport(ERROR,
                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
                 errmsg("must be member of role \"%s\"",
                        GetUserNameFromId(role, false))));
}
```

giveaway attackの具体的な定義は不明だが、giveawayには「〔秘密の情報を〕漏らす、ばらす、暴露する」という意味があるらしい by 英辞郎。

推測するに、データベースを作成しようとしているユーザが所有者のロールのメンバでないのに作れてしまうと、自分が管理できない範囲での挙動を許してしまうというリスクがあると思われる。

以上。