Add "pg_database_owner" default role.
authorNoah Misch <[email protected]>
Fri, 26 Mar 2021 17:42:17 +0000 (10:42 -0700)
committerNoah Misch <[email protected]>
Fri, 26 Mar 2021 17:42:17 +0000 (10:42 -0700)
Membership consists, implicitly, of the current database owner.  Expect
use in template databases.  Once pg_database_owner has rights within a
template, each owner of a database instantiated from that template will
exercise those rights.

Reviewed by John Naylor.

Discussion: https://postgr.es/m/20201228043148[email protected]

doc/src/sgml/catalogs.sgml
doc/src/sgml/user-manag.sgml
src/backend/catalog/information_schema.sql
src/backend/commands/user.c
src/backend/utils/adt/acl.c
src/backend/utils/cache/catcache.c
src/bin/psql/describe.c
src/include/catalog/catversion.h
src/include/catalog/pg_authid.dat
src/test/regress/expected/privileges.out
src/test/regress/sql/privileges.sql

index cd00d9e3bb0ec57dca8d8b21f215454c7e0ad3b2..0f8703af5a585a8865d26202e4fc42e341ecc00b 100644 (file)
@@ -10138,6 +10138,9 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
    <primary>pg_group</primary>
   </indexterm>
 
+  <!-- Unlike information_schema.applicable_roles, this shows no members for
+       pg_database_owner.  The v8.1 catalog would have shown no members if
+       that role had existed at the time. -->
   <para>
    The view <structname>pg_group</structname> exists for backwards
    compatibility: it emulates a catalog that existed in
index a7c187896bd9405642555505e63517a7a75d3ae8..6920f2db2b2389a6efbcc24c087e9ff623326bae 100644 (file)
@@ -540,6 +540,10 @@ DROP ROLE doomed_role;
        <literal>pg_read_all_stats</literal> and
        <literal>pg_stat_scan_tables</literal>.</entry>
       </row>
+      <row>
+       <entry>pg_database_owner</entry>
+       <entry>None.  Membership consists, implicitly, of the current database owner.</entry>
+      </row>
       <row>
        <entry>pg_signal_backend</entry>
        <entry>Signal another backend to cancel a query or terminate its session.</entry>
@@ -572,6 +576,17 @@ DROP ROLE doomed_role;
   other system information normally restricted to superusers.
   </para>
 
+  <para>
+  The <literal>pg_database_owner</literal> role has one implicit,
+  situation-dependent member, namely the owner of the current database.  The
+  role conveys no rights at first.  Like any role, it can own objects or
+  receive grants of access privileges.  Consequently, once
+  <literal>pg_database_owner</literal> has rights within a template database,
+  each owner of a database instantiated from that template will exercise those
+  rights.  <literal>pg_database_owner</literal> cannot be a member of any
+  role, and it cannot have non-implicit members.
+  </para>
+
   <para>
   The <literal>pg_signal_backend</literal> role is intended to allow
   administrators to enable trusted, but non-superuser, roles to send signals
index 513cb9a69cf9117779cf3d0c72ac16d7248fca69..941a9f664c918ad33927dd55cbcf4180458bd86e 100644 (file)
@@ -255,7 +255,14 @@ CREATE VIEW applicable_roles AS
     SELECT CAST(a.rolname AS sql_identifier) AS grantee,
            CAST(b.rolname AS sql_identifier) AS role_name,
            CAST(CASE WHEN m.admin_option THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_grantable
-    FROM pg_auth_members m
+    FROM (SELECT member, roleid, admin_option FROM pg_auth_members
+          -- This UNION could be UNION ALL, but UNION works even if we start
+          -- to allow explicit pg_database_owner membership.
+          UNION
+          SELECT datdba, pg_authid.oid, false
+          FROM pg_database, pg_authid
+          WHERE datname = current_database() AND rolname = 'pg_database_owner'
+         )  m
          JOIN pg_authid a ON (m.member = a.oid)
          JOIN pg_authid b ON (m.roleid = b.oid)
     WHERE pg_has_role(a.oid, 'USAGE');
index ed243e3d1415fa007b0b5be44df16c9563ff9c61..e91fa4c78c95bb6ab018f40c0f501dbd267c4b6e 100644 (file)
@@ -1496,6 +1496,18 @@ AddRoleMems(const char *rolename, Oid roleid,
                            rolename)));
    }
 
+   /*
+    * The charter of pg_database_owner is to have exactly one, implicit,
+    * situation-dependent member.  There's no technical need for this
+    * restriction.  (One could lift it and take the further step of making
+    * pg_database_ownercheck() equivalent to has_privs_of_role(roleid,
+    * DEFAULT_ROLE_DATABASE_OWNER), in which case explicit,
+    * situation-independent members could act as the owner of any database.)
+    */
+   if (roleid == DEFAULT_ROLE_DATABASE_OWNER)
+       ereport(ERROR,
+               errmsg("role \"%s\" cannot have explicit members", rolename));
+
    /*
     * The role membership grantor of record has little significance at
     * present.  Nonetheless, inasmuch as users might look to it for a crude
@@ -1524,6 +1536,30 @@ AddRoleMems(const char *rolename, Oid roleid,
        bool        new_record_nulls[Natts_pg_auth_members];
        bool        new_record_repl[Natts_pg_auth_members];
 
+       /*
+        * pg_database_owner is never a role member.  Lifting this restriction
+        * would require a policy decision about membership loops.  One could
+        * prevent loops, which would include making "ALTER DATABASE x OWNER
+        * TO proposed_datdba" fail if is_member_of_role(pg_database_owner,
+        * proposed_datdba).  Hence, gaining a membership could reduce what a
+        * role could do.  Alternately, one could allow these memberships to
+        * complete loops.  A role could then have actual WITH ADMIN OPTION on
+        * itself, prompting a decision about is_admin_of_role() treatment of
+        * the case.
+        *
+        * Lifting this restriction also has policy implications for ownership
+        * of shared objects (databases and tablespaces).  We allow such
+        * ownership, but we might find cause to ban it in the future.
+        * Designing such a ban would more troublesome if the design had to
+        * address pg_database_owner being a member of role FOO that owns a
+        * shared object.  (The effect of such ownership is that any owner of
+        * another database can act as the owner of affected shared objects.)
+        */
+       if (memberid == DEFAULT_ROLE_DATABASE_OWNER)
+           ereport(ERROR,
+                   errmsg("role \"%s\" cannot be a member of any role",
+                          get_rolespec_name(memberRole)));
+
        /*
         * Refuse creation of membership loops, including the trivial case
         * where a role is made a member of itself.  We do this by checking to
index e6b4bdbd7685b590e3802438ed753906417f0586..9955c7c5c06cdd3de2a8be3104bee32ef93f5941 100644 (file)
@@ -22,6 +22,7 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_type.h"
 #include "commands/dbcommands.h"
 #include "commands/proclang.h"
@@ -68,6 +69,7 @@ enum RoleRecurseType
 };
 static Oid cached_role[] = {InvalidOid, InvalidOid};
 static List *cached_roles[] = {NIL, NIL};
+static uint32 cached_db_hash;
 
 
 static const char *getid(const char *s, char *n);
@@ -4665,10 +4667,14 @@ initialize_acl(void)
 {
    if (!IsBootstrapProcessingMode())
    {
+       cached_db_hash =
+           GetSysCacheHashValue1(DATABASEOID,
+                                 ObjectIdGetDatum(MyDatabaseId));
+
        /*
         * In normal mode, set a callback on any syscache invalidation of rows
-        * of pg_auth_members (for roles_is_member_of()) or pg_authid (for
-        * has_rolinherit())
+        * of pg_auth_members (for roles_is_member_of()), pg_authid (for
+        * has_rolinherit()), or pg_database (for roles_is_member_of())
         */
        CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
                                      RoleMembershipCacheCallback,
@@ -4676,6 +4682,9 @@ initialize_acl(void)
        CacheRegisterSyscacheCallback(AUTHOID,
                                      RoleMembershipCacheCallback,
                                      (Datum) 0);
+       CacheRegisterSyscacheCallback(DATABASEOID,
+                                     RoleMembershipCacheCallback,
+                                     (Datum) 0);
    }
 }
 
@@ -4686,6 +4695,13 @@ initialize_acl(void)
 static void
 RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
 {
+   if (cacheid == DATABASEOID &&
+       hashvalue != cached_db_hash &&
+       hashvalue != 0)
+   {
+       return;                 /* ignore pg_database changes for other DBs */
+   }
+
    /* Force membership caches to be recomputed on next use */
    cached_role[ROLERECURSE_PRIVS] = InvalidOid;
    cached_role[ROLERECURSE_MEMBERS] = InvalidOid;
@@ -4728,6 +4744,7 @@ static List *
 roles_is_member_of(Oid roleid, enum RoleRecurseType type,
                   Oid admin_of, bool *is_admin)
 {
+   Oid         dba;
    List       *roles_list;
    ListCell   *l;
    List       *new_cached_roles;
@@ -4740,6 +4757,24 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
        OidIsValid(cached_role[type]))
        return cached_roles[type];
 
+   /*
+    * Role expansion happens in a non-database backend when guc.c checks
+    * DEFAULT_ROLE_READ_ALL_SETTINGS for a physical walsender SHOW command.
+    * In that case, no role gets pg_database_owner.
+    */
+   if (!OidIsValid(MyDatabaseId))
+       dba = InvalidOid;
+   else
+   {
+       HeapTuple   dbtup;
+
+       dbtup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
+       if (!HeapTupleIsValid(dbtup))
+           elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
+       dba = ((Form_pg_database) GETSTRUCT(dbtup))->datdba;
+       ReleaseSysCache(dbtup);
+   }
+
    /*
     * Find all the roles that roleid is a member of, including multi-level
     * recursion.  The role itself will always be the first element of the
@@ -4787,6 +4822,11 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
            roles_list = list_append_unique_oid(roles_list, otherid);
        }
        ReleaseSysCacheList(memlist);
+
+       /* implement pg_database_owner implicit membership */
+       if (memberid == dba && OidIsValid(dba))
+           roles_list = list_append_unique_oid(roles_list,
+                                               DEFAULT_ROLE_DATABASE_OWNER);
    }
 
    /*
index 55c944589818cc5dbc4d25c89ceb3e6bd0bf655b..4fbdc62d8c75965abcc166788030ca703c8760a5 100644 (file)
@@ -1076,8 +1076,9 @@ InitCatCachePhase2(CatCache *cache, bool touch_index)
  *     criticalRelcachesBuilt), we don't have to worry anymore.
  *
  *     Similarly, during backend startup we have to be able to use the
- *     pg_authid and pg_auth_members syscaches for authentication even if
- *     we don't yet have relcache entries for those catalogs' indexes.
+ *     pg_authid, pg_auth_members and pg_database syscaches for
+ *     authentication even if we don't yet have relcache entries for those
+ *     catalogs' indexes.
  */
 static bool
 IndexScanOK(CatCache *cache, ScanKey cur_skey)
@@ -1110,6 +1111,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
        case AUTHNAME:
        case AUTHOID:
        case AUTHMEMMEMROLE:
+       case DATABASEOID:
 
            /*
             * Protect authentication lookups occurring before relcache has
index c9f7118a5dcc14798cba7cf91d87f12b391a244a..e56cc43e1114f43dd6523d686cb76a71f26a483c 100644 (file)
@@ -3557,6 +3557,7 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 
    printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
    printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
+   /* ignores implicit memberships from superuser & pg_database_owner */
    printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
 
    if (verbose && pset.sversion >= 80200)
index 943f9aee9fdfd00d31dd4d063883119dc55e662d..474ee2982b89946d611fdc5b49cc9066b073a01e 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202103264
+#define CATALOG_VERSION_NO 202103265
 
 #endif
index 87d917ffc3843eea6b48e92ac6dec26567bb1468..4c2bf972ecc99e654d7453d0a4efdc0407b26d63 100644 (file)
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
   rolpassword => '_null_', rolvaliduntil => '_null_' },
+{ oid => '8778', oid_symbol => 'DEFAULT_ROLE_DATABASE_OWNER',
+  rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't',
+  rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+  rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+  rolpassword => '_null_', rolvaliduntil => '_null_' },
 { oid => '3373', oid_symbol => 'DEFAULT_ROLE_MONITOR',
   rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
index 46a69fc0dc9dc5c1a7d990d155ff74bb7e05a1cd..4903371991f7f19a0c50436f252bfcc875e9682c 100644 (file)
@@ -1719,6 +1719,67 @@ SELECT * FROM pg_largeobject LIMIT 0;
 SET SESSION AUTHORIZATION regress_priv_user1;
 SELECT * FROM pg_largeobject LIMIT 0;          -- to be denied
 ERROR:  permission denied for table pg_largeobject
+-- test pg_database_owner
+RESET SESSION AUTHORIZATION;
+GRANT pg_database_owner TO regress_priv_user1;
+ERROR:  role "pg_database_owner" cannot have explicit members
+GRANT regress_priv_user1 TO pg_database_owner;
+ERROR:  role "pg_database_owner" cannot be a member of any role
+CREATE TABLE datdba_only ();
+ALTER TABLE datdba_only OWNER TO pg_database_owner;
+REVOKE DELETE ON datdba_only FROM pg_database_owner;
+SELECT
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv,
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem,
+   pg_has_role('regress_priv_user1', 'pg_database_owner',
+               'MEMBER WITH ADMIN OPTION') as admin;
+ priv | mem | admin 
+------+-----+-------
+ f    | f   | f
+(1 row)
+
+BEGIN;
+DO $$BEGIN EXECUTE format(
+   'ALTER DATABASE %I OWNER TO regress_priv_group2', current_catalog); END$$;
+SELECT
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv,
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem,
+   pg_has_role('regress_priv_user1', 'pg_database_owner',
+               'MEMBER WITH ADMIN OPTION') as admin;
+ priv | mem | admin 
+------+-----+-------
+ t    | t   | f
+(1 row)
+
+SET SESSION AUTHORIZATION regress_priv_user1;
+TABLE information_schema.enabled_roles ORDER BY role_name COLLATE "C";
+      role_name      
+---------------------
+ pg_database_owner
+ regress_priv_group2
+ regress_priv_user1
+(3 rows)
+
+TABLE information_schema.applicable_roles ORDER BY role_name COLLATE "C";
+       grantee       |      role_name      | is_grantable 
+---------------------+---------------------+--------------
+ regress_priv_group2 | pg_database_owner   | NO
+ regress_priv_user1  | regress_priv_group2 | NO
+(2 rows)
+
+INSERT INTO datdba_only DEFAULT VALUES;
+SAVEPOINT q; DELETE FROM datdba_only; ROLLBACK TO q;
+ERROR:  permission denied for table datdba_only
+SET SESSION AUTHORIZATION regress_priv_user2;
+TABLE information_schema.enabled_roles;
+     role_name      
+--------------------
+ regress_priv_user2
+(1 row)
+
+INSERT INTO datdba_only DEFAULT VALUES;
+ERROR:  permission denied for table datdba_only
+ROLLBACK;
 -- test default ACLs
 \c -
 CREATE SCHEMA testns;
index 6277140cfd3e38b5d1696eaa6c91067279ec9caf..8dcd2199e0d4dbfc449363ce94af7b5ab2d898e5 100644 (file)
@@ -1034,6 +1034,37 @@ SELECT * FROM pg_largeobject LIMIT 0;
 SET SESSION AUTHORIZATION regress_priv_user1;
 SELECT * FROM pg_largeobject LIMIT 0;          -- to be denied
 
+-- test pg_database_owner
+RESET SESSION AUTHORIZATION;
+GRANT pg_database_owner TO regress_priv_user1;
+GRANT regress_priv_user1 TO pg_database_owner;
+CREATE TABLE datdba_only ();
+ALTER TABLE datdba_only OWNER TO pg_database_owner;
+REVOKE DELETE ON datdba_only FROM pg_database_owner;
+SELECT
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv,
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem,
+   pg_has_role('regress_priv_user1', 'pg_database_owner',
+               'MEMBER WITH ADMIN OPTION') as admin;
+
+BEGIN;
+DO $$BEGIN EXECUTE format(
+   'ALTER DATABASE %I OWNER TO regress_priv_group2', current_catalog); END$$;
+SELECT
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv,
+   pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem,
+   pg_has_role('regress_priv_user1', 'pg_database_owner',
+               'MEMBER WITH ADMIN OPTION') as admin;
+SET SESSION AUTHORIZATION regress_priv_user1;
+TABLE information_schema.enabled_roles ORDER BY role_name COLLATE "C";
+TABLE information_schema.applicable_roles ORDER BY role_name COLLATE "C";
+INSERT INTO datdba_only DEFAULT VALUES;
+SAVEPOINT q; DELETE FROM datdba_only; ROLLBACK TO q;
+SET SESSION AUTHORIZATION regress_priv_user2;
+TABLE information_schema.enabled_roles;
+INSERT INTO datdba_only DEFAULT VALUES;
+ROLLBACK;
+
 -- test default ACLs
 \c -