Thank you so much for the backtrace!

This latest patch should address by moving the dumpRoleMembership call to
before the pointer is closed.

On Sat, Jan 22, 2022 at 1:11 AM Julien Rouhaud <rjuju...@gmail.com> wrote:

> Hi,
>
> On Fri, Jan 21, 2022 at 07:01:21PM -0800, Kenaniah Cerny wrote:
> > Thanks for the feedback.
> >
> > I have attached an alternate version of the v5 patch that incorporates
> the
> > suggested changes to the documentation and DRYs up some of the acl.c code
> > for comparison. As for the databaseOid / InvalidOid parameter, I'm open
> to
> > any suggestions for how to make that even cleaner, but am currently at a
> > loss as to how that would look.
> >
> > CI is showing a failure to run pg_dump on just the Linux - Debian
> Bullseye
> > job (https://cirrus-ci.com/task/5265343722553344). Does anyone have any
> > ideas as to where I should look in order to debug that?
>
> Did you try to reproduce it on some GNU/Linux system?  FTR I had and I get
> a
> segfault in pg_dumpall:
>
> (gdb) bt
> #0  __pthread_kill_implementation (threadid=<optimized out>,
> signo=signo@entry=6, no_tid=no_tid@entry=0) at pthread_kill.c:44
> #1  0x00007f329e7e40cf in __pthread_kill_internal (signo=6,
> threadid=<optimized out>) at pthread_kill.c:78
> #2  0x00007f329e7987a2 in __GI_raise (sig=sig@entry=6) at
> ../sysdeps/posix/raise.c:26
> #3  0x00007f329e783449 in __GI_abort () at abort.c:79
> #4  0x00007f329e7d85d8 in __libc_message (action=action@entry=do_abort,
> fmt=fmt@entry=0x7f329e90b6aa "%s\n") at ../sysdeps/posix/libc_fatal.c:155
> #5  0x00007f329e7edcfa in malloc_printerr (str=str@entry=0x7f329e9092c3
> "free(): invalid pointer") at malloc.c:5536
> #6  0x00007f329e7ef504 in _int_free (av=<optimized out>, p=<optimized
> out>, have_lock=0) at malloc.c:4327
> #7  0x00007f329e7f1f81 in __GI___libc_free (mem=<optimized out>) at
> malloc.c:3279
> #8  0x00007f329e7dbec5 in __GI__IO_free_backup_area 
> (fp=fp@entry=0x561775f126c0)
> at genops.c:190
> #9  0x00007f329e7db6af in _IO_new_file_overflow (f=0x561775f126c0, ch=-1)
> at fileops.c:758
> #10 0x00007f329e7da7be in _IO_new_file_xsputn (n=2, data=<optimized out>,
> f=<optimized out>) at
> /usr/src/debug/sys-libs/glibc-2.34-r4/glibc-2.34/libio/libioP.h:947
> #11 _IO_new_file_xsputn (f=0x561775f126c0, data=<optimized out>, n=2) at
> fileops.c:1197
> #12 0x00007f329e7cfd32 in __GI__IO_fwrite (buf=0x7ffc90bb0ac0, size=1,
> count=2, fp=0x561775f126c0) at
> /usr/src/debug/sys-libs/glibc-2.34-r4/glibc-2.34/libio/libioP.h:947
> #13 0x000056177483c758 in flushbuffer (target=0x7ffc90bb0a90) at
> snprintf.c:310
> #14 0x000056177483c4e8 in pg_vfprintf (stream=0x561775f126c0,
> fmt=0x561774840dec "\n\n", args=0x7ffc90bb0f00) at snprintf.c:259
> #15 0x000056177483c5ce in pg_fprintf (stream=0x561775f126c0,
> fmt=0x561774840dec "\n\n") at snprintf.c:270
> #16 0x0000561774831893 in dumpRoleMembership (conn=0x561775f09600,
> databaseId=0x561775f152d2 "1") at pg_dumpall.c:991
> #17 0x0000561774832426 in dumpDatabases (conn=0x561775f09600) at
> pg_dumpall.c:1332
> #18 0x000056177483049e in main (argc=3, argv=0x7ffc90bb1658) at
> pg_dumpall.c:596
>
> I didn't look in detail, but:
>
> @@ -1323,6 +1327,10 @@ dumpDatabases(PGconn *conn)
>             exit_nicely(1);
>         }
>
> +       /* Dump database-specific roles if server is running 15.0 or later
> */
> +       if (server_version >= 150000)
> +           dumpRoleMembership(conn, dbid);
> +
>
> Isn't that trying print to OPF after the possible fclose(OPF) a bit before
> and
> before it's reopened?
>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1e65c426b28e..a0beec6136e6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1642,11 +1642,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
 
   <para>
-   Because user identities are cluster-wide,
-   <structname>pg_auth_members</structname>
-   is shared across all databases of a cluster: there is only one
-   copy of <structname>pg_auth_members</structname> per cluster, not
-   one per database.
+   User identities are cluster-wide, but role memberships can be either
+   cluster-wide or database-specific (as specified by the value of the
+   <structfield>dbid</structfield> column).  The <structname>pg_auth_members</structname>
+   catalog is shared across all databases of a cluster.
   </para>
 
   <table>
@@ -1703,6 +1702,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <structfield>roleid</structfield> to others
       </para></entry>
      </row>
+
+      <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dbid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-database"><structname>pg_database</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       ID of the database that this membership is constrained to; zero if membership is cluster-wide
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2e5..98bcfed5f507 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -93,6 +93,7 @@ GRANT { USAGE | ALL [ PRIVILEGES ] }
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
 GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replaceable class="parameter">role_specification</replaceable> [, ...]
+    [ IN DATABASE <replaceable class="parameter">database_name</replaceable> | IN CURRENT DATABASE ]
     [ WITH ADMIN OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
@@ -243,7 +244,23 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    This variant of the <command>GRANT</command> command grants membership
    in a role to one or more other roles.  Membership in a role is significant
    because it conveys the privileges granted to a role to each of its
-   members.
+   members.  Membership is effective cluster-wide unless otherwise constrained
+   through the use of a database-specific clause.  Both database-specific and cluster-wide
+   versions of a role membership grant may exist at the same time.  In the event that
+   multiple grants apply, the membership privileges conferred are additive.
+  </para>
+
+  <para>
+   If <literal>IN DATABASE <replaceable class="parameter">database_name</replaceable></literal>
+   is specified, membership in <replaceable class="parameter">role_name</replaceable>
+   will be effective only when the recipient is connected to the database specified by
+   <replaceable class="parameter">database_name</replaceable>.
+  </para>
+
+  <para>
+   If <literal>IN CURRENT DATABASE</literal> is specified, the membership in
+   <replaceable class="parameter">role_name</replaceable> will be effective only when the
+   recipient is connected to the same database that the grant was issued in.
   </para>
 
   <para>
@@ -270,6 +287,10 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    does not allow the noise word <literal>GROUP</literal>
    in <replaceable class="parameter">role_specification</replaceable>.
   </para>
+
+  <para>
+   See <xref linkend="role-membership"/> for more information about role memberships.
+  </para>
  </refsect2>
  </refsect1>
 
@@ -391,10 +412,18 @@ GRANT ALL PRIVILEGES ON kinds TO manuel;
   </para>
 
   <para>
-   Grant membership in role <literal>admins</literal> to user <literal>joe</literal>:
+   Grant cluster-wide membership in role <literal>admins</literal> to user <literal>joe</literal>:
 
 <programlisting>
 GRANT admins TO joe;
+</programlisting></para>
+
+  <para>
+   Grant read and write access to user <literal>alice</literal> in the database
+   named <literal>sales</literal>:
+
+<programlisting>
+GRANT pg_read_all_data, pg_write_all_data TO alice IN DATABASE sales;
 </programlisting></para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 3014c864ea3c..b5013f004451 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -120,6 +120,7 @@ REVOKE [ GRANT OPTION FOR ]
 
 REVOKE [ ADMIN OPTION FOR ]
     <replaceable class="parameter">role_name</replaceable> [, ...] FROM <replaceable class="parameter">role_specification</replaceable> [, ...]
+    [ IN DATABASE <replaceable class="parameter">database_name</replaceable> | IN CURRENT DATABASE ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
     [ CASCADE | RESTRICT ]
 
@@ -298,6 +299,14 @@ REVOKE ALL PRIVILEGES ON kinds FROM manuel;
 
 <programlisting>
 REVOKE admins FROM joe;
+</programlisting></para>
+
+  <para>
+   Revoke write access for user <literal>bob</literal> from the <literal>sales</literal>
+   database:
+
+<programlisting>
+REVOKE pg_write_all_data FROM bob IN DATABASE sales;
 </programlisting></para>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c78..9a32d27945a4 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -308,6 +308,11 @@ CREATE ROLE <replaceable>name</replaceable>;
 <synopsis>
 GRANT <replaceable>group_role</replaceable> TO <replaceable>role1</replaceable>, ... ;
 REVOKE <replaceable>group_role</replaceable> FROM <replaceable>role1</replaceable>, ... ;
+</synopsis>
+   Role membership can also be granted and revoked within the context of a specific database:
+<synopsis>
+GRANT <replaceable>group_role</replaceable> TO <replaceable>role1</replaceable>, ... IN CURRENT DATABASE;
+REVOKE <replaceable>group_role</replaceable> FROM <replaceable>role1</replaceable>, ... IN DATABASE <replaceable>database_name</replaceable>;
 </synopsis>
    You can grant membership to other group roles, too (since there isn't
    really any distinction between group roles and non-group roles).  The
@@ -398,9 +403,6 @@ RESET ROLE;
    <command>SET ROLE admin</command>.
   </para>
 
-  <para>
-  </para>
-
   <para>
    To destroy a group role, use <link
    linkend="sql-droprole"><command>DROP ROLE</command></link>:
@@ -639,7 +641,7 @@ DROP ROLE doomed_role;
   </para>
 
   <para>
-   Administrators can grant access to these roles to users using the
+   Administrators can grant cluster-wide access to these roles to users using the
    <link linkend="sql-grant"><command>GRANT</command></link> command, for example:
 
 <programlisting>
@@ -647,6 +649,14 @@ GRANT pg_signal_backend TO admin_user;
 </programlisting>
   </para>
 
+  <para>
+   Access can also be granted within the context of a specific database,
+   for example:
+
+<programlisting>
+GRANT pg_read_all_data TO reporting_user IN DATABASE sales;
+</programlisting>
+  </para>
  </sect1>
 
  <sect1 id="perm-functions">
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index dfd5fb669eef..c4b2325a6c05 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -258,8 +258,9 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdRolnameIndexId ||
 		relationId == AuthIdOidIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
+		relationId == AuthMemDbMemRoleIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
 		relationId == DatabaseNameIndexId ||
 		relationId == DatabaseOidIndexId ||
 		relationId == SharedDescriptionObjIndexId ||
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index da8345561d8f..822d3ab5c96c 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -46,6 +46,7 @@
 #include "commands/defrem.h"
 #include "commands/seclabel.h"
 #include "commands/tablespace.h"
+#include "commands/user.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -915,6 +916,11 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	DeleteSharedComments(db_id, DatabaseRelationId);
 	DeleteSharedSecurityLabel(db_id, DatabaseRelationId);
 
+	/*
+	 * Delete any roles memberships directly associated with this database.
+	 */
+	DropDatabaseSpecificRoles(db_id);
+
 	/*
 	 * Remove settings associated with this database
 	 */
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246bb2..e7f3a6b9104c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -51,10 +51,10 @@ check_password_hook_type check_password_hook = NULL;
 
 static void AddRoleMems(const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
-						Oid grantorId, bool admin_opt);
+						Oid grantorId, bool admin_opt, Oid dbid);
 static void DelRoleMems(const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
-						bool admin_opt);
+						bool admin_opt, Oid dbid);
 
 
 /* Check if current user has createrole privileges */
@@ -453,7 +453,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 			AddRoleMems(oldrolename, oldroleid,
 						thisrole_list,
 						thisrole_oidlist,
-						GetUserId(), false);
+						GetUserId(), false, InvalidOid);
 
 			ReleaseSysCache(oldroletup);
 		}
@@ -465,10 +465,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	AddRoleMems(stmt->role, roleid,
 				adminmembers, roleSpecsToIds(adminmembers),
-				GetUserId(), true);
+				GetUserId(), true, InvalidOid);
 	AddRoleMems(stmt->role, roleid,
 				rolemembers, roleSpecsToIds(rolemembers),
-				GetUserId(), false);
+				GetUserId(), false, InvalidOid);
 
 	/* Post creation hook for new role */
 	InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -805,11 +805,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		if (stmt->action == +1)		/* add members to role */
 			AddRoleMems(rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						GetUserId(), false);
+						GetUserId(), false, InvalidOid);
 		else if (stmt->action == -1)	/* drop members from role */
 			DelRoleMems(rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						false);
+						false, InvalidOid);
 	}
 
 	/*
@@ -1025,7 +1025,7 @@ DropRole(DropRoleStmt *stmt)
 					BTEqualStrategyNumber, F_OIDEQ,
 					ObjectIdGetDatum(roleid));
 
-		sscan = systable_beginscan(pg_auth_members_rel, AuthMemRoleMemIndexId,
+		sscan = systable_beginscan(pg_auth_members_rel, AuthMemRoleMemDbIndexId,
 								   true, NULL, 1, &scankey);
 
 		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
@@ -1040,7 +1040,7 @@ DropRole(DropRoleStmt *stmt)
 					BTEqualStrategyNumber, F_OIDEQ,
 					ObjectIdGetDatum(roleid));
 
-		sscan = systable_beginscan(pg_auth_members_rel, AuthMemMemRoleIndexId,
+		sscan = systable_beginscan(pg_auth_members_rel, AuthMemMemRoleDbIndexId,
 								   true, NULL, 1, &scankey);
 
 		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
@@ -1230,6 +1230,17 @@ GrantRole(GrantRoleStmt *stmt)
 	Oid			grantor;
 	List	   *grantee_ids;
 	ListCell   *item;
+	Oid dbid;
+
+	/* Determine if this grant/revoke is database-specific */
+	if (stmt->database == NULL) {
+		dbid = InvalidOid;
+	} else if (strcmp(stmt->database, "") == 0) {
+		dbid = MyDatabaseId;
+	} else {
+		dbid = get_database_oid(stmt->database, false);
+	}
+
 
 	if (stmt->grantor)
 		grantor = get_rolespec_oid(stmt->grantor, false);
@@ -1264,11 +1275,11 @@ GrantRole(GrantRoleStmt *stmt)
 		if (stmt->is_grant)
 			AddRoleMems(rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						grantor, stmt->admin_opt);
+						grantor, stmt->admin_opt, dbid);
 		else
 			DelRoleMems(rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						stmt->admin_opt);
+						stmt->admin_opt, dbid);
 	}
 
 	/*
@@ -1375,7 +1386,7 @@ roleSpecsToIds(List *memberNames)
 static void
 AddRoleMems(const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			Oid grantorId, bool admin_opt)
+			Oid grantorId, bool admin_opt, Oid dbid)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1402,7 +1413,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 	else
 	{
 		if (!have_createrole_privilege() &&
-			!is_admin_of_role(grantorId, roleid))
+			!is_admin_of_role(grantorId, roleid, dbid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("must have admin option on role \"%s\"",
@@ -1490,16 +1501,23 @@ AddRoleMems(const char *rolename, Oid roleid,
 		 * Check if entry for this role/member already exists; if so, give
 		 * warning unless we are adding admin option.
 		 */
-		authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+		authmem_tuple = SearchSysCache3(AUTHMEMROLEMEMDB,
 										ObjectIdGetDatum(roleid),
-										ObjectIdGetDatum(memberid));
+										ObjectIdGetDatum(memberid),
+										ObjectIdGetDatum(dbid));
 		if (HeapTupleIsValid(authmem_tuple) &&
 			(!admin_opt ||
 			 ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
 		{
-			ereport(NOTICE,
-					(errmsg("role \"%s\" is already a member of role \"%s\"",
-							get_rolespec_name(memberRole), rolename)));
+			if (dbid == InvalidOid) {
+				ereport(NOTICE,
+						(errmsg("role \"%s\" is already a member of role \"%s\"",
+								get_rolespec_name(memberRole), rolename)));
+			} else {
+				ereport(NOTICE,
+						(errmsg("role \"%s\" is already a member of role \"%s\" in database \"%s\"",
+								get_rolespec_name(memberRole), rolename, get_database_name(dbid))));
+			}
 			ReleaseSysCache(authmem_tuple);
 			continue;
 		}
@@ -1513,6 +1531,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 		new_record[Anum_pg_auth_members_member - 1] = ObjectIdGetDatum(memberid);
 		new_record[Anum_pg_auth_members_grantor - 1] = ObjectIdGetDatum(grantorId);
 		new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(admin_opt);
+		new_record[Anum_pg_auth_members_dbid - 1] = ObjectIdGetDatum(dbid);
 
 		if (HeapTupleIsValid(authmem_tuple))
 		{
@@ -1553,7 +1572,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 static void
 DelRoleMems(const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			bool admin_opt)
+			bool admin_opt, Oid dbid)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1580,7 +1599,7 @@ DelRoleMems(const char *rolename, Oid roleid,
 	else
 	{
 		if (!have_createrole_privilege() &&
-			!is_admin_of_role(GetUserId(), roleid))
+			!is_admin_of_role(GetUserId(), roleid, dbid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("must have admin option on role \"%s\"",
@@ -1599,14 +1618,21 @@ DelRoleMems(const char *rolename, Oid roleid,
 		/*
 		 * Find entry for this role/member
 		 */
-		authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+		authmem_tuple = SearchSysCache3(AUTHMEMROLEMEMDB,
 										ObjectIdGetDatum(roleid),
-										ObjectIdGetDatum(memberid));
+										ObjectIdGetDatum(memberid),
+										ObjectIdGetDatum(dbid));
 		if (!HeapTupleIsValid(authmem_tuple))
 		{
-			ereport(WARNING,
-					(errmsg("role \"%s\" is not a member of role \"%s\"",
-							get_rolespec_name(memberRole), rolename)));
+			if (dbid == InvalidOid){
+				ereport(WARNING,
+						(errmsg("role \"%s\" is not a member of role \"%s\"",
+								get_rolespec_name(memberRole), rolename)));
+			} else {
+				ereport(WARNING,
+						(errmsg("role \"%s\" is not a member of role \"%s\" in database \"%s\"",
+								get_rolespec_name(memberRole), rolename, get_database_name(dbid))));
+			}
 			continue;
 		}
 
@@ -1648,3 +1674,42 @@ DelRoleMems(const char *rolename, Oid roleid,
 	 */
 	table_close(pg_authmem_rel, NoLock);
 }
+
+/*
+ * DropDatabaseSpecificRoles
+ *
+ * Delete pg_auth_members entries corresponding to a database that's being
+ * dropped.
+ */
+void
+DropDatabaseSpecificRoles(Oid databaseId)
+{
+	Relation	pg_authmem_rel;
+	ScanKeyData key[1];
+	SysScanDesc scan;
+	HeapTuple	tup;
+
+	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+
+	/*
+	 * First, delete all the entries that have the database Oid in the dbid
+	 * field.
+	 */
+	ScanKeyInit(&key[0],
+				Anum_pg_auth_members_dbid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(databaseId));
+	/* We leave the other index fields unspecified */
+
+	scan = systable_beginscan(pg_authmem_rel, AuthMemDbMemRoleIndexId, true,
+							  NULL, 1, key);
+
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		CatalogTupleDelete(pg_authmem_rel, &tup->t_self);
+	}
+
+	systable_endscan(scan);
+
+	table_close(pg_authmem_rel, RowExclusiveLock);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 90b5da51c950..db2e950f95f8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3437,6 +3437,7 @@ _copyGrantRoleStmt(const GrantRoleStmt *from)
 
 	COPY_NODE_FIELD(granted_roles);
 	COPY_NODE_FIELD(grantee_roles);
+	COPY_SCALAR_FIELD(database);
 	COPY_SCALAR_FIELD(is_grant);
 	COPY_SCALAR_FIELD(admin_opt);
 	COPY_NODE_FIELD(grantor);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 06345da3ba84..66e15ca79a2b 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1215,6 +1215,7 @@ _equalGrantRoleStmt(const GrantRoleStmt *a, const GrantRoleStmt *b)
 {
 	COMPARE_NODE_FIELD(granted_roles);
 	COMPARE_NODE_FIELD(grantee_roles);
+	COMPARE_SCALAR_FIELD(database);
 	COMPARE_SCALAR_FIELD(is_grant);
 	COMPARE_SCALAR_FIELD(admin_opt);
 	COMPARE_NODE_FIELD(grantor);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b5966712ce14..f6f31102c009 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -361,7 +361,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <str>		opt_type
 %type <str>		foreign_server_version opt_foreign_server_version
-%type <str>		opt_in_database
+%type <str>		opt_in_database opt_grant_in_database
 
 %type <str>		OptSchemaName
 %type <list>	OptSchemaEltList
@@ -7252,6 +7252,11 @@ grantee:
 		;
 
 
+opt_grant_in_database:
+			IN_P CURRENT_P DATABASE { $$ = ""; }
+			| opt_in_database { $$ = $1; }
+		;
+
 opt_grant_grant_option:
 			WITH GRANT OPTION { $$ = true; }
 			| /*EMPTY*/ { $$ = false; }
@@ -7264,37 +7269,40 @@ opt_grant_grant_option:
  *****************************************************************************/
 
 GrantRoleStmt:
-			GRANT privilege_list TO role_list opt_grant_admin_option opt_granted_by
+			GRANT privilege_list TO role_list opt_grant_in_database opt_grant_admin_option opt_granted_by
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 					n->is_grant = true;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->admin_opt = $5;
-					n->grantor = $6;
+					n->database = $5;
+					n->admin_opt = $6;
+					n->grantor = $7;
 					$$ = (Node*)n;
 				}
 		;
 
 RevokeRoleStmt:
-			REVOKE privilege_list FROM role_list opt_granted_by opt_drop_behavior
+			REVOKE privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 					n->is_grant = false;
 					n->admin_opt = false;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->behavior = $6;
+					n->database = $5;
+					n->behavior = $7;
 					$$ = (Node*)n;
 				}
-			| REVOKE ADMIN OPTION FOR privilege_list FROM role_list opt_granted_by opt_drop_behavior
+			| REVOKE ADMIN OPTION FOR privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 					n->is_grant = false;
 					n->admin_opt = true;
 					n->granted_roles = $5;
 					n->grantee_roles = $7;
-					n->behavior = $9;
+					n->database = $8;
+					n->behavior = $10;
 					$$ = (Node*)n;
 				}
 		;
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156cb4..be37285d4753 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4624,7 +4624,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 		 * session and call stack.  That suits two-argument pg_has_role(), but
 		 * it gives the three-argument version a lamentable whimsy.
 		 */
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -4658,7 +4658,7 @@ initialize_acl(void)
 		 * of pg_auth_members (for roles_is_member_of()), pg_authid (for
 		 * has_rolinherit()), or pg_database (for roles_is_member_of())
 		 */
-		CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
+		CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB,
 									  RoleMembershipCacheCallback,
 									  (Datum) 0);
 		CacheRegisterSyscacheCallback(AUTHOID,
@@ -4707,6 +4707,48 @@ has_rolinherit(Oid roleid)
 }
 
 
+/*
+ * Appends role memberships to the list of roles
+ */
+static void
+append_role_memberships(List *roles_list, bool *is_admin, Oid admin_of,
+				   Oid memberid, Oid targetDatabaseId, Oid databaseId)
+{
+	CatCList   *memlist;
+	int			i;
+
+	/* Find roles that memberid is directly a member of */
+	memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+									ObjectIdGetDatum(targetDatabaseId),
+									ObjectIdGetDatum(memberid));
+	for (i = 0; i < memlist->n_members; i++)
+	{
+		HeapTuple	tup = &memlist->members[i]->tuple;
+		Oid			otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid;
+
+		/*
+		 * While otherid==InvalidOid shouldn't appear in the catalog, the
+		 * OidIsValid() avoids crashing if that arises. This reports if
+		 * the admin option has been granted.
+		 */
+		if (otherid == admin_of &&
+			((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
+			((Form_pg_auth_members) GETSTRUCT(tup))->dbid == databaseId &&
+			OidIsValid(admin_of))
+			*is_admin = true;
+
+		/*
+		 * Even though there shouldn't be any loops in the membership
+		 * graph, we must test for having already seen this role. It is
+		 * legal for instance to have both A->B and A->C->B.
+		 */
+		roles_list = list_append_unique_oid(roles_list, otherid);
+	}
+	ReleaseSysCacheList(memlist);
+
+}
+
+
 /*
  * Get a list of roles that the specified roleid is a member of
  *
@@ -4724,7 +4766,7 @@ has_rolinherit(Oid roleid)
  */
 static List *
 roles_is_member_of(Oid roleid, enum RoleRecurseType type,
-				   Oid admin_of, bool *is_admin)
+				   Oid admin_of, bool *is_admin, Oid databaseId)
 {
 	Oid			dba;
 	List	   *roles_list;
@@ -4773,37 +4815,15 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 	foreach(l, roles_list)
 	{
 		Oid			memberid = lfirst_oid(l);
-		CatCList   *memlist;
-		int			i;
 
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
-									  ObjectIdGetDatum(memberid));
-		for (i = 0; i < memlist->n_members; i++)
-		{
-			HeapTuple	tup = &memlist->members[i]->tuple;
-			Oid			otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid;
-
-			/*
-			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
-			 */
-			if (otherid == admin_of &&
-				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
-				OidIsValid(admin_of))
-				*is_admin = true;
-
-			/*
-			 * Even though there shouldn't be any loops in the membership
-			 * graph, we must test for having already seen this role. It is
-			 * legal for instance to have both A->B and A->C->B.
-			 */
-			roles_list = list_append_unique_oid(roles_list, otherid);
-		}
-		ReleaseSysCacheList(memlist);
+		/* Find roles that memberid is directly a member of globally */
+		append_role_memberships(roles_list, is_admin, admin_of, memberid, InvalidOid, InvalidOid);
+
+		/* Find roles that memberid is directly a member of in the current database */
+		append_role_memberships(roles_list, is_admin, admin_of, memberid, MyDatabaseId, databaseId);
 
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
@@ -4855,7 +4875,7 @@ has_privs_of_role(Oid member, Oid role)
 	 * multi-level recursion, then see if target role is any one of them.
 	 */
 	return list_member_oid(roles_is_member_of(member, ROLERECURSE_PRIVS,
-											  InvalidOid, NULL),
+											  InvalidOid, NULL, InvalidOid),
 						   role);
 }
 
@@ -4881,7 +4901,7 @@ is_member_of_role(Oid member, Oid role)
 	 * recursion, then see if target role is any one of them.
 	 */
 	return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS,
-											  InvalidOid, NULL),
+											  InvalidOid, NULL, InvalidOid),
 						   role);
 }
 
@@ -4917,7 +4937,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
 	 * recursion, then see if target role is any one of them.
 	 */
 	return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS,
-											  InvalidOid, NULL),
+											  InvalidOid, NULL, InvalidOid),
 						   role);
 }
 
@@ -4928,7 +4948,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
  * or a superuser?
  */
 bool
-is_admin_of_role(Oid member, Oid role)
+is_admin_of_role(Oid member, Oid role, Oid databaseId)
 {
 	bool		result = false;
 
@@ -4968,7 +4988,8 @@ is_admin_of_role(Oid member, Oid role)
 		return member == GetSessionUserId() &&
 			!InLocalUserIdChange() && !InSecurityRestrictedOperation();
 
-	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
+	/* Check for WITH ADMIN OPTION either globally or for the given database */
+	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result, databaseId);
 	return result;
 }
 
@@ -5044,7 +5065,7 @@ select_best_grantor(Oid roleId, AclMode privileges,
 	 * doesn't query any role memberships.
 	 */
 	roles_list = roles_is_member_of(roleId, ROLERECURSE_PRIVS,
-									InvalidOid, NULL);
+									InvalidOid, NULL, InvalidOid);
 
 	/* initialize candidate result as default */
 	*grantorId = roleId;
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index eb8308808937..d98948120cbf 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1110,7 +1110,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHMEMMEMROLE:
+		case AUTHMEMMEMROLEDB:
 		case DATABASEOID:
 
 			/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2e760e8a3bdc..80463e614569 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4060,7 +4060,7 @@ RelationCacheInitializePhase3(void)
 							AuthIdRelationId);
 		load_critical_index(AuthIdOidIndexId,
 							AuthIdRelationId);
-		load_critical_index(AuthMemMemRoleIndexId,
+		load_critical_index(AuthMemMemRoleDbIndexId,
 							AuthMemRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4e7819f1e2d..491dd3ab86fb 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -210,24 +210,35 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		128
 	},
-	{AuthMemRelationId,			/* AUTHMEMMEMROLE */
-		AuthMemMemRoleIndexId,
-		2,
+	{AuthMemRelationId,			/* AUTHMEMDBMEMROLE */
+		AuthMemDbMemRoleIndexId,
+		3,
 		{
+			Anum_pg_auth_members_dbid,
 			Anum_pg_auth_members_member,
 			Anum_pg_auth_members_roleid,
-			0,
 			0
 		},
 		8
 	},
-	{AuthMemRelationId,			/* AUTHMEMROLEMEM */
-		AuthMemRoleMemIndexId,
-		2,
+	{AuthMemRelationId,			/* AUTHMEMMEMROLEDB */
+		AuthMemMemRoleDbIndexId,
+		3,
+		{
+			Anum_pg_auth_members_member,
+			Anum_pg_auth_members_roleid,
+			Anum_pg_auth_members_dbid,
+			0
+		},
+		8
+	},
+	{AuthMemRelationId,			/* AUTHMEMROLEMEMDB */
+		AuthMemRoleMemDbIndexId,
+		3,
 		{
 			Anum_pg_auth_members_roleid,
 			Anum_pg_auth_members_member,
-			0,
+			Anum_pg_auth_members_dbid,
 			0
 		},
 		8
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 10383c713fee..94d61e538335 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -35,7 +35,7 @@ static void help(void);
 
 static void dropRoles(PGconn *conn);
 static void dumpRoles(PGconn *conn);
-static void dumpRoleMembership(PGconn *conn);
+static void dumpRoleMembership(PGconn *conn, const char *databaseId);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
 static void dropDBs(PGconn *conn);
@@ -584,7 +584,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 		}
 
 		/* Dump tablespaces */
@@ -937,7 +937,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -951,8 +951,9 @@ dumpRoleMembership(PGconn *conn)
 					  "LEFT JOIN %s ur on ur.oid = a.roleid "
 					  "LEFT JOIN %s um on um.oid = a.member "
 					  "LEFT JOIN %s ug on ug.oid = a.grantor "
-					  "WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
-					  "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+					  "WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_') "
+						"AND a.dbid = %s"
+					  "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog, databaseId);
 	res = executeQuery(conn, buf->data);
 
 	if (PQntuples(res) > 0)
@@ -966,6 +967,8 @@ dumpRoleMembership(PGconn *conn)
 
 		fprintf(OPF, "GRANT %s", fmtId(roleid));
 		fprintf(OPF, " TO %s", fmtId(member));
+		if (strcmp(databaseId, "0") != 0)
+			fprintf(OPF, " IN CURRENT DATABASE");
 		if (*option == 't')
 			fprintf(OPF, " WITH ADMIN OPTION");
 
@@ -1262,7 +1265,7 @@ dumpDatabases(PGconn *conn)
 	 * doesn't have some failure mode with --clean.
 	 */
 	res = executeQuery(conn,
-					   "SELECT datname "
+					   "SELECT datname, oid "
 					   "FROM pg_database d "
 					   "WHERE datallowconn "
 					   "ORDER BY (datname <> 'template1'), datname");
@@ -1273,6 +1276,7 @@ dumpDatabases(PGconn *conn)
 	for (i = 0; i < PQntuples(res); i++)
 	{
 		char	   *dbname = PQgetvalue(res, i, 0);
+		char	   *dbid = PQgetvalue(res, i, 1);
 		const char *create_opts;
 		int			ret;
 
@@ -1313,6 +1317,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 15.0 or later */
+		if (server_version >= 150000)
+			dumpRoleMembership(conn, dbid);
+
 		if (filename)
 			fclose(OPF);
 
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133d5..26d0d5381e8b 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -33,6 +33,7 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
 	Oid			member BKI_LOOKUP(pg_authid);	/* ID of a member of that role */
 	Oid			grantor BKI_LOOKUP(pg_authid);	/* who granted the membership */
 	bool		admin_option;	/* granted with admin option? */
+	Oid			dbid BKI_LOOKUP_OPT(pg_database); /* ID of a database this mapping is effective in */
 } FormData_pg_auth_members;
 
 /* ----------------
@@ -42,7 +43,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
  */
 typedef FormData_pg_auth_members *Form_pg_auth_members;
 
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_dbid_index, 2694, AuthMemRoleMemDbIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, dbid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_dbid_index, 2695, AuthMemMemRoleDbIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, dbid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_dbid_member_role_index, 4715, AuthMemDbMemRoleIndexId, on pg_auth_members using btree(dbid oid_ops, member oid_ops, roleid oid_ops));
 
 #endif							/* PG_AUTH_MEMBERS_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65fd2..f5f8e4c00055 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,6 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
 extern void DropOwnedObjects(DropOwnedStmt *stmt);
 extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
 extern List *roleSpecsToIds(List *memberNames);
+extern void DropDatabaseSpecificRoles(Oid databaseId);
 
 #endif							/* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3e9bdc781f9b..ad0a64c1f2c0 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2092,6 +2092,8 @@ typedef struct GrantRoleStmt
 	NodeTag		type;
 	List	   *granted_roles;	/* list of roles to be granted/revoked */
 	List	   *grantee_roles;	/* list of member roles to add/delete */
+	char		*database;		/* name of DB this grant applies to
+		NULL = global, "" = current database, otherwise a named database */
 	bool		is_grant;		/* true = GRANT, false = REVOKE */
 	bool		admin_opt;		/* with admin option */
 	RoleSpec   *grantor;		/* set grantor to other than current role */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 1ce4c5556e70..57dfe78f1673 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -208,7 +208,7 @@ extern int	aclmembers(const Acl *acl, Oid **roleids);
 extern bool has_privs_of_role(Oid member, Oid role);
 extern bool is_member_of_role(Oid member, Oid role);
 extern bool is_member_of_role_nosuper(Oid member, Oid role);
-extern bool is_admin_of_role(Oid member, Oid role);
+extern bool is_admin_of_role(Oid member, Oid role, Oid databaseId);
 extern void check_is_member_of_role(Oid member, Oid role);
 extern Oid	get_role_oid(const char *rolename, bool missing_ok);
 extern Oid	get_role_oid_or_public(const char *rolename);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 9c1a76e8bb66..6ad506357aa2 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -39,8 +39,9 @@ enum SysCacheIdentifier
 	AMPROCNUM,
 	ATTNAME,
 	ATTNUM,
-	AUTHMEMMEMROLE,
-	AUTHMEMROLEMEM,
+	AUTHMEMDBMEMROLE,
+	AUTHMEMMEMROLEDB,
+	AUTHMEMROLEMEMDB,
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
diff --git a/src/test/modules/unsafe_tests/Makefile b/src/test/modules/unsafe_tests/Makefile
index 3ecf5fcfc5bb..6cf403afcd04 100644
--- a/src/test/modules/unsafe_tests/Makefile
+++ b/src/test/modules/unsafe_tests/Makefile
@@ -1,6 +1,6 @@
 # src/test/modules/unsafe_tests/Makefile
 
-REGRESS = rolenames alter_system_table
+REGRESS = rolenames alter_system_table role_membership
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
diff --git a/src/test/modules/unsafe_tests/expected/role_membership.out b/src/test/modules/unsafe_tests/expected/role_membership.out
new file mode 100644
index 000000000000..2ea9ba093d13
--- /dev/null
+++ b/src/test/modules/unsafe_tests/expected/role_membership.out
@@ -0,0 +1,537 @@
+CREATE ROLE role_admin LOGIN SUPERUSER;
+\connect postgres role_admin
+CREATE FUNCTION check_memberships()
+ RETURNS TABLE (role name, member name, grantor name, admin_option boolean, datname name)
+ AS $$
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  g.rolname as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+LEFT JOIN pg_roles r ON r.oid = a.roleid
+LEFT JOIN pg_roles m ON m.oid = a.member
+LEFT JOIN pg_roles g ON g.oid = a.grantor
+LEFT JOIN pg_database d ON d.oid = a.dbid
+WHERE
+  m.rolname LIKE 'role_%'
+ORDER BY
+  1, 2, 5
+$$ LANGUAGE SQL;
+-- Populate test databases
+\connect template1
+CREATE TABLE data AS SELECT generate_series(1, 3);
+CREATE DATABASE db_1;
+CREATE DATABASE db_2;
+CREATE DATABASE db_3;
+CREATE DATABASE db_4;
+-- Read all cluster-wide with admin option
+CREATE ROLE role_read_all_with_admin;
+GRANT pg_read_all_data TO role_read_all_with_admin WITH ADMIN OPTION;
+-- Read all in databases 1 and 2
+CREATE ROLE role_read_12;
+GRANT pg_read_all_data TO role_read_12 IN DATABASE db_1;
+GRANT pg_read_all_data TO role_read_12 IN DATABASE db_2;
+-- Read all in databases 3 and 4 with admin option
+CREATE ROLE role_read_34;
+GRANT pg_read_all_data TO role_read_34 IN DATABASE db_3 WITH ADMIN OPTION;
+GRANT pg_read_all_data TO role_read_34 IN DATABASE db_4 WITH ADMIN OPTION;
+-- Inherits read all in databases 3 and 4
+CREATE ROLE role_inherited_34;
+GRANT role_read_34 TO role_inherited_34;
+-- Inherits read all in database 3
+CREATE ROLE role_inherited_3;
+GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3;
+-- No inherit
+CREATE ROLE role_read_all_noinherit NOINHERIT;
+GRANT role_read_all_with_admin TO role_read_all_noinherit;
+-- No inherit in databases 1 and 2
+CREATE ROLE role_read_12_noinherit NOINHERIT;
+GRANT role_read_12 TO role_read_12_noinherit;
+-- Alternate syntax
+CREATE ROLE role_read_template1;
+GRANT pg_read_all_data TO role_read_template1, role_read_all_noinherit IN CURRENT DATABASE;
+-- Failure due to missing database
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE non_existent; -- error
+ERROR:  database "non_existent" does not exist
+-- Should warn on duplicate grants
+GRANT pg_read_all_data TO role_read_all_with_admin; -- notice
+NOTICE:  role "role_read_all_with_admin" is already a member of role "pg_read_all_data"
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1; -- notice
+NOTICE:  role "role_read_template1" is already a member of role "pg_read_all_data" in database "template1"
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- notice
+NOTICE:  role "role_read_template1" is already a member of role "pg_read_all_data" in database "template1"
+-- Check membership table
+\connect postgres role_admin
+SELECT * FROM check_memberships();
+           role           |          member          |  grantor   | admin_option |  datname  
+--------------------------+--------------------------+------------+--------------+-----------
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_data         | role_read_template1      | role_admin | t            | template1
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(11 rows)
+
+-- Test membership privileges (db_1)
+\connect db_1
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_12;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- success
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE role_read_34; -- success
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE role_read_34; -- error
+ERROR:  permission denied to set role "role_read_34"
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+-- Test membership privileges (db_2)
+\connect db_2
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_12;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- success
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE role_read_34; -- success
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE role_read_34; -- error
+ERROR:  permission denied to set role "role_read_34"
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+-- Test membership privileges (db_3)
+\connect db_3
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_12;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12; -- error
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+-- Test membership privileges (db_4)
+\connect db_4
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_read_12;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE role_read_34; -- error
+ERROR:  permission denied to set role "role_read_34"
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE role_read_12; -- error
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+\connect postgres role_admin
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent
+SELECT * FROM check_memberships();
+           role           |          member          |  grantor   | admin_option |  datname  
+--------------------------+--------------------------+------------+--------------+-----------
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_data         | role_read_template1      | role_admin | f            | template1
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(11 rows)
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- success
+REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- warning
+WARNING:  role "role_read_template1" is not a member of role "pg_read_all_data" in database "template1"
+SELECT * FROM check_memberships();
+           role           |          member          |  grantor   | admin_option |  datname  
+--------------------------+--------------------------+------------+--------------+-----------
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(10 rows)
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM role_read_12; -- warning
+WARNING:  role "role_read_12" is not a member of role "pg_read_all_data"
+SELECT * FROM check_memberships();
+           role           |          member          |  grantor   | admin_option |  datname  
+--------------------------+--------------------------+------------+--------------+-----------
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(10 rows)
+
+-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases
+CREATE ROLE role_granted;
+SET SESSION AUTHORIZATION role_read_all_with_admin;
+GRANT pg_read_all_data TO role_granted; -- success
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_1; -- success
+GRANT role_read_34 TO role_granted; -- error
+ERROR:  must have admin option on role "role_read_34"
+SELECT * FROM check_memberships();
+           role           |          member          |         grantor          | admin_option |  datname  
+--------------------------+--------------------------+--------------------------+--------------+-----------
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_1
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | postgres
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | 
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin               | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin               | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin               | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_34             | role_inherited_3         | role_admin               | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(13 rows)
+
+-- Ensure database-specific admin option can only grant within that database
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+\connect db_3
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- notice
+NOTICE:  role "role_granted" is already a member of role "pg_read_all_data" in database "db_3"
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+\connect db_4
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error
+ERROR:  must have admin option on role "pg_read_all_data"
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- notice
+NOTICE:  role "role_granted" is already a member of role "pg_read_all_data" in database "db_4"
+\connect postgres role_admin
+SELECT * FROM check_memberships();
+           role           |          member          |         grantor          | admin_option |  datname  
+--------------------------+--------------------------+--------------------------+--------------+-----------
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_1
+ pg_read_all_data         | role_granted             | role_read_34             | f            | db_3
+ pg_read_all_data         | role_granted             | role_read_34             | f            | db_4
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | postgres
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | 
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin               | t            | db_3
+ pg_read_all_data         | role_read_34             | role_admin               | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin               | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_34             | role_inherited_3         | role_admin               | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(15 rows)
+
+-- Should clean up the membership table when dropping a database
+\connect postgres role_admin
+DROP DATABASE db_3;
+SELECT * FROM check_memberships();
+           role           |          member          |         grantor          | admin_option |  datname  
+--------------------------+--------------------------+--------------------------+--------------+-----------
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_1
+ pg_read_all_data         | role_granted             | role_read_34             | f            | db_4
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | postgres
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | 
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_read_34             | role_admin               | t            | db_4
+ pg_read_all_data         | role_read_all_noinherit  | role_admin               | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(12 rows)
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE role_read_34;
+SELECT * FROM check_memberships();
+           role           |          member          |         grantor          | admin_option |  datname  
+--------------------------+--------------------------+--------------------------+--------------+-----------
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_1
+ pg_read_all_data         | role_granted             |                          | f            | db_4
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | postgres
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | 
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_1
+ pg_read_all_data         | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_read_all_noinherit  | role_admin               | f            | template1
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(10 rows)
+
diff --git a/src/test/modules/unsafe_tests/sql/role_membership.sql b/src/test/modules/unsafe_tests/sql/role_membership.sql
new file mode 100644
index 000000000000..66b6ec6bf7a9
--- /dev/null
+++ b/src/test/modules/unsafe_tests/sql/role_membership.sql
@@ -0,0 +1,291 @@
+CREATE ROLE role_admin LOGIN SUPERUSER;
+
+\connect postgres role_admin
+
+CREATE FUNCTION check_memberships()
+ RETURNS TABLE (role name, member name, grantor name, admin_option boolean, datname name)
+ AS $$
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  g.rolname as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+LEFT JOIN pg_roles r ON r.oid = a.roleid
+LEFT JOIN pg_roles m ON m.oid = a.member
+LEFT JOIN pg_roles g ON g.oid = a.grantor
+LEFT JOIN pg_database d ON d.oid = a.dbid
+WHERE
+  m.rolname LIKE 'role_%'
+ORDER BY
+  1, 2, 5
+$$ LANGUAGE SQL;
+
+-- Populate test databases
+\connect template1
+CREATE TABLE data AS SELECT generate_series(1, 3);
+
+CREATE DATABASE db_1;
+CREATE DATABASE db_2;
+CREATE DATABASE db_3;
+CREATE DATABASE db_4;
+
+-- Read all cluster-wide with admin option
+CREATE ROLE role_read_all_with_admin;
+GRANT pg_read_all_data TO role_read_all_with_admin WITH ADMIN OPTION;
+
+-- Read all in databases 1 and 2
+CREATE ROLE role_read_12;
+GRANT pg_read_all_data TO role_read_12 IN DATABASE db_1;
+GRANT pg_read_all_data TO role_read_12 IN DATABASE db_2;
+
+-- Read all in databases 3 and 4 with admin option
+CREATE ROLE role_read_34;
+GRANT pg_read_all_data TO role_read_34 IN DATABASE db_3 WITH ADMIN OPTION;
+GRANT pg_read_all_data TO role_read_34 IN DATABASE db_4 WITH ADMIN OPTION;
+
+-- Inherits read all in databases 3 and 4
+CREATE ROLE role_inherited_34;
+GRANT role_read_34 TO role_inherited_34;
+
+-- Inherits read all in database 3
+CREATE ROLE role_inherited_3;
+GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3;
+
+-- No inherit
+CREATE ROLE role_read_all_noinherit NOINHERIT;
+GRANT role_read_all_with_admin TO role_read_all_noinherit;
+
+-- No inherit in databases 1 and 2
+CREATE ROLE role_read_12_noinherit NOINHERIT;
+GRANT role_read_12 TO role_read_12_noinherit;
+
+-- Alternate syntax
+CREATE ROLE role_read_template1;
+GRANT pg_read_all_data TO role_read_template1, role_read_all_noinherit IN CURRENT DATABASE;
+
+-- Failure due to missing database
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE non_existent; -- error
+
+-- Should warn on duplicate grants
+GRANT pg_read_all_data TO role_read_all_with_admin; -- notice
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1; -- notice
+
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- notice
+
+-- Check membership table
+\connect postgres role_admin
+SELECT * FROM check_memberships();
+
+-- Test membership privileges (db_1)
+\connect db_1
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE role_read_12;
+SELECT * FROM data; -- success
+SET ROLE role_read_34;
+SELECT * FROM data; -- error
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- error
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- success
+
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+SET ROLE role_read_34; -- success
+
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE role_read_34; -- error
+
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12; -- success
+SELECT * FROM data; -- success
+
+-- Test membership privileges (db_2)
+\connect db_2
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE role_read_12;
+SELECT * FROM data; -- success
+SET ROLE role_read_34;
+SELECT * FROM data; -- error
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- error
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- success
+
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+SET ROLE role_read_34; -- success
+
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE role_read_34; -- error
+
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12; -- success
+SELECT * FROM data; -- success
+
+-- Test membership privileges (db_3)
+\connect db_3
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE role_read_12;
+SELECT * FROM data; -- error
+SET ROLE role_read_34;
+SELECT * FROM data; -- success
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- success
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- success
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- error
+
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12; -- error
+SELECT * FROM data; -- error
+
+-- Test membership privileges (db_4)
+\connect db_4
+SET ROLE role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE role_read_12;
+SELECT * FROM data; -- error
+SET ROLE role_read_34;
+SELECT * FROM data; -- success
+SET ROLE role_inherited_34;
+SELECT * FROM data; -- success
+SET ROLE role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION role_read_12;
+SET ROLE pg_read_all_data; -- error
+
+SET SESSION AUTHORIZATION role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE role_read_34; -- success
+
+SET SESSION AUTHORIZATION role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE role_read_34; -- error
+
+SET SESSION AUTHORIZATION role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE role_read_12; -- error
+SELECT * FROM data; -- error
+
+\connect postgres role_admin
+
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent
+SELECT * FROM check_memberships();
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- success
+REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- warning
+SELECT * FROM check_memberships();
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM role_read_12; -- warning
+SELECT * FROM check_memberships();
+
+-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases
+CREATE ROLE role_granted;
+SET SESSION AUTHORIZATION role_read_all_with_admin;
+GRANT pg_read_all_data TO role_granted; -- success
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_1; -- success
+GRANT role_read_34 TO role_granted; -- error
+SELECT * FROM check_memberships();
+
+-- Ensure database-specific admin option can only grant within that database
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- error
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+
+\connect db_3
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- notice
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+
+\connect db_4
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- notice
+
+\connect postgres role_admin
+SELECT * FROM check_memberships();
+
+-- Should clean up the membership table when dropping a database
+\connect postgres role_admin
+DROP DATABASE db_3;
+SELECT * FROM check_memberships();
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE role_read_34;
+SELECT * FROM check_memberships();
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3e..79fb69059b68 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_members {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}

Reply via email to