Proposal: allow database-specific role memberships

Started by Kenaniah Cernyover 4 years ago33 messages
#1Kenaniah Cerny
kenaniah@gmail.com
1 attachment(s)

Hi all,

In building off of prior art regarding the 'pg_read_all_data' and
'pg_write_all_data' roles, I would like to propose an extension to roles
that would allow for database-specific role memberships (for the purpose of
granting database-specific privileges) as an additional layer of
abstraction.

= Problem =

There is currently no mechanism to grant the privileges afforded by the
default roles on a per-database basis. This makes it difficult to cleanly
accomplish permissions such as 'db_datareader' and 'db_datawriter' (which
are database-level roles in SQL Server that respectively grant read and
write access within a specific database).

The recently-added 'pg_read_all_data' and 'pg_write_all_data' work
similarly to 'db_datareader' and 'db_datawriter', but work cluster-wide.

= Proposal =

I propose an extension to the GRANT / REVOKE syntax as well as an
additional column within pg_auth_members in order to track role memberships
that are only effective within the specified database.

Role membership (and subsequent privileges) would be calculated using the
following algorithm:
- Check for regular (cluster-wide) role membership (the way it works today)
- Check for database-specific role membership based on the
currently-connected database

Attached is a proof of concept patch that implements this.

= Implementation Notes =

- A new column (pg_auth_members.dbid) in the system catalog that is set to
InvalidOid for regular role memberships, or the oid of the given database
for database-specific role memberships.

- GRANT / REVOKE syntax has been extended to include the ability to specify
a database-specific role membership:
- "IN DATABASE database_name" would cause the GRANT to be applicable only
within the specified database.
- "IN CURRENT DATABASE" would cause the GRANT to be applicable only
within the currently-connected database.
- Omission of the clause would create a regular (cluster-wide) role
membership (the way it works today).

The proposed syntax (applies to REVOKE as well):

GRANT role_name [, ...] TO role_specification [, ...]
[ IN DATABASE database_name | IN CURRENT DATABASE ]
[ WITH ADMIN OPTION ]
[ GRANTED BY role_specification ]

- DROP DATABASE has been updated to clean up any database-specific role
memberships that are associated with the database being dropped.

- pg_dump_all will dump database-specific role memberships using the "IN
CURRENT DATABASE" syntax. (pg_dump has not been modified)

- is_admin_of_role()'s signature has been updated to include the oid of the
database being checked as a third argument. This now returns true if the
member has WITH ADMIN OPTION either globally or for the database given.

- roles_is_member_of() will additionally include any database-specific role
memberships for the database being checked in its result set.

= Example =

CREATE DATABASE accounting;
CREATE DATABASE sales;

CREATE ROLE alice;
CREATE ROLE bob;

-- Alice is granted read-all privileges cluster-wide (nothing new here)
GRANT pg_read_all_data TO alice;

-- Bob is granted read-all privileges to just the accounting database
GRANT pg_read_all_data TO bob IN DATABASE accounting;

= Final Thoughts =

This is my first attempt at contributing code to the project, and I would
not self-identify as a C programmer. I wanted to get a sense for how
receptive the contributors and community would be to this proposal and
whether there were any concerns or preferred alternatives before I further
embark on a fool's errand.

Thoughts?

Thanks,

-- Kenaniah

Attachments:

poc-database-role-membership-v1.patchapplication/octet-stream; name=poc-database-role-membership-v1.patchDownload
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index aa7d4d5456..1a9fe4da50 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 029fab48df..d6fcd5af2f 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"
@@ -918,6 +919,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 aa69821be4..b4eb2623a9 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);
@@ -833,11 +833,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);
 
 	/*
 	 * Close pg_authid, but keep lock till commit.
@@ -1052,7 +1052,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)))
@@ -1067,7 +1067,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)))
@@ -1257,6 +1257,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);
@@ -1291,11 +1302,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);
 	}
 
 	/*
@@ -1402,7 +1413,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;
@@ -1429,7 +1440,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\"",
@@ -1517,16 +1528,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;
 		}
@@ -1540,6 +1558,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))
 		{
@@ -1580,7 +1599,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;
@@ -1607,7 +1626,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\"",
@@ -1626,14 +1645,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;
 		}
 
@@ -1675,3 +1701,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 70e9e54d3e..4786663e1f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3429,6 +3429,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 19eff20102..c77af820a6 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 08f1bf1031..02a724d820 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -340,7 +340,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
@@ -7157,6 +7157,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; }
@@ -7169,37 +7174,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 67f8b29434..ca51b21a88 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,
@@ -4724,7 +4724,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;
@@ -4779,8 +4779,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
+		/* Find roles that memberid is directly a member of globally */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(InvalidOid),
 									  ObjectIdGetDatum(memberid));
 		for (i = 0; i < memlist->n_members; i++)
 		{
@@ -4789,7 +4790,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 			/*
 			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
+			 * OidIsValid() avoids crashing if that arises. This reports if
+			 * the admin option has been granted globally.
 			 */
 			if (otherid == admin_of &&
 				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
@@ -4805,6 +4807,35 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		}
 		ReleaseSysCacheList(memlist);
 
+		/* Find roles that memberid is directly a member of for the current database */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(MyDatabaseId),
+									  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 for a specific database.
+			 */
+			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);
+
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
 			roles_list = list_append_unique_oid(roles_list,
@@ -4855,7 +4886,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 +4912,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 +4948,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 +4959,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 +4999,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 +5076,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 4fbdc62d8c..0fc161875c 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 13d9994af3..a4c5ab93ff 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -3981,7 +3981,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 d6cb78dea8..d80a933ad1 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -209,24 +209,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 c29101704a..e60108bdc6 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 dumpGroups(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -583,7 +583,7 @@ main(int argc, char *argv[])
 
 			/* Dump role memberships --- need different method for pre-8.1 */
 			if (server_version >= 80100)
-				dumpRoleMembership(conn);
+				dumpRoleMembership(conn, "0");
 			else
 				dumpGroups(conn);
 		}
@@ -998,7 +998,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -1012,8 +1012,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)
@@ -1027,6 +1028,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");
 
@@ -1477,7 +1480,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");
@@ -1488,6 +1491,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;
 
@@ -1538,6 +1542,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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3253b8751b..892f0d2a37 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202109101
+#define CATALOG_VERSION_NO	202110032
 
 #endif
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index b9d3ffd1c5..9c75eb496a 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 0b7a3cd65f..f5f8e4c000 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 3138877553..d3f35e5fff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2090,6 +2090,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 af771c901d..ac45de17da 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 d74a348600..853f4daf41 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/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 1461e947cd..4b43969292 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}
#2David G. Johnston
david.g.johnston@gmail.com
In reply to: Kenaniah Cerny (#1)
Re: Proposal: allow database-specific role memberships

On Sun, Oct 10, 2021 at 2:29 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

In building off of prior art regarding the 'pg_read_all_data' and
'pg_write_all_data' roles, I would like to propose an extension to roles
that would allow for database-specific role memberships (for the purpose of
granting database-specific privileges) as an additional layer of
abstraction.

= Problem =

There is currently no mechanism to grant the privileges afforded by the
default roles on a per-database basis. This makes it difficult to cleanly
accomplish permissions such as 'db_datareader' and 'db_datawriter' (which
are database-level roles in SQL Server that respectively grant read and
write access within a specific database).

The recently-added 'pg_read_all_data' and 'pg_write_all_data' work
similarly to 'db_datareader' and 'db_datawriter', but work cluster-wide.

My first impression is that this is more complex than just restricting
which databases users are allowed to connect to. The added flexibility
this would provide has some benefit but doesn't seem worth the added
complexity.

David J.

#3Stephen Frost
sfrost@snowman.net
In reply to: David G. Johnston (#2)
Re: Proposal: allow database-specific role memberships

Greetings,

* David G. Johnston (david.g.johnston@gmail.com) wrote:

On Sun, Oct 10, 2021 at 2:29 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

In building off of prior art regarding the 'pg_read_all_data' and
'pg_write_all_data' roles, I would like to propose an extension to roles
that would allow for database-specific role memberships (for the purpose of
granting database-specific privileges) as an additional layer of
abstraction.

= Problem =

There is currently no mechanism to grant the privileges afforded by the
default roles on a per-database basis. This makes it difficult to cleanly
accomplish permissions such as 'db_datareader' and 'db_datawriter' (which
are database-level roles in SQL Server that respectively grant read and
write access within a specific database).

The recently-added 'pg_read_all_data' and 'pg_write_all_data' work
similarly to 'db_datareader' and 'db_datawriter', but work cluster-wide.

My first impression is that this is more complex than just restricting
which databases users are allowed to connect to. The added flexibility
this would provide has some benefit but doesn't seem worth the added
complexity.

Having an ability to GRANT predefined roles within a particular database
is certainly something that I'd considered when adding the pg_read/write
data roles. I'm not super thrilled with the idea of adding a column to
pg_auth_members just for predefined roles though and I'm not sure that
such role membership makes sense for non-predefined roles. Would
welcome input from others as to if that's something that would make
sense or if folks have asked about that before. We'd need to carefully
think through what this means in terms of making sure we don't end up
with any loops too.

Does seem like we'd probably need to change more than just what's
suggested here so that you could, for example, ask "is role X a member
of role Y in database Z" without actually being connected to database Z.
That's just a matter of adding some functions though- the existing
functions would work with just the assumption that you're asking about
within the current database.

I don't think "just don't grant access to those other databases"
is actually a proper answer- there is certainly a use-case for "I want
user X to have read access to all tables in *this* database, and also
allow them to connect to some other database but not have that same
level of access there."

Thanks,

Stephen

#4Isaac Morland
isaac.morland@gmail.com
In reply to: Stephen Frost (#3)
Re: Proposal: allow database-specific role memberships

On Mon, 11 Oct 2021 at 11:01, Stephen Frost <sfrost@snowman.net> wrote:

Having an ability to GRANT predefined roles within a particular database
is certainly something that I'd considered when adding the pg_read/write
data roles. I'm not super thrilled with the idea of adding a column to
pg_auth_members just for predefined roles though and I'm not sure that
such role membership makes sense for non-predefined roles. Would
welcome input from others as to if that's something that would make
sense or if folks have asked about that before. We'd need to carefully
think through what this means in terms of making sure we don't end up
with any loops too.

I think the ability to grant a role within a particular database would be
useful. For example, imagine I have a dev/testing instance with multiple
databases, each a copy of production modified in some way for different
testing purposes. For example, one might be scrambled data (to make the
testing data non- or less- confidential); another might be limited to data
from the last year (to reduce the size of everything); another might be
limited to 1% of all the customers (to reduce the size in a different way);
and of course these could be combined.

It’s easy to imagine that I might want to grant a user the ability to
connect to all of these databases, but to have different privileges. For
example, maybe they have read_confidential_data in the scrambled database
but not in the reduced-but-not-scrambled databases. But maybe they have a
lesser level of access to these databases, so just using the connect
privilege won't do the job.

I’ve already found it a bit weird that I can set per-role, per-database
settings (e.g search_path), and of course privileges on individual objects,
but not which roles the role is a member of.

I haven’t thought about implementation at all however. The thought occurs
to me that the union of all the role memberships in all the database should
form a directed acyclic graph. In other words, you could not have X a
member of Y (possibly indirectly) in one database while Y is a member of X
in another database; the role memberships in each database would then be a
subset of the complete graph of memberships.

#5David G. Johnston
david.g.johnston@gmail.com
In reply to: Stephen Frost (#3)
Re: Proposal: allow database-specific role memberships

On Monday, October 11, 2021, Stephen Frost <sfrost@snowman.net> wrote:

I don't think "just don't grant access to those other databases"
is actually a proper answer- there is certainly a use-case for "I want
user X to have read access to all tables in *this* database, and also
allow them to connect to some other database but not have that same
level of access there."

Sure, that has a benefit. But creating a second user for the other
database and putting the onus on the user to use the correct credentials
when logging into a particular database is a valid option - it is in fact
the status quo. Due to the complexity of adding a whole new grant
dimension to the system the status quo is an appealing option. Annoyance
factor aside it technically solves the per-database permissions problem put
forth.

David J.

#6Stephen Frost
sfrost@snowman.net
In reply to: David G. Johnston (#5)
Re: Proposal: allow database-specific role memberships

Greetings,

* David G. Johnston (david.g.johnston@gmail.com) wrote:

On Monday, October 11, 2021, Stephen Frost <sfrost@snowman.net> wrote:

I don't think "just don't grant access to those other databases"
is actually a proper answer- there is certainly a use-case for "I want
user X to have read access to all tables in *this* database, and also
allow them to connect to some other database but not have that same
level of access there."

Sure, that has a benefit. But creating a second user for the other
database and putting the onus on the user to use the correct credentials
when logging into a particular database is a valid option - it is in fact
the status quo. Due to the complexity of adding a whole new grant
dimension to the system the status quo is an appealing option. Annoyance
factor aside it technically solves the per-database permissions problem put
forth.

I disagree entirely that forcing users to have multiple accounts and to
deal with "using the correct one" is at all reasonable. That's an utter
hack that results in a given user having multiple different accounts-
something that gets really ugly to deal with in enterprise deployments
which use any kind of centralized authentication system.

No, that's not a solution. Perhaps there's another way to implement
this capability that is simpler than what's proposed here, but saying
"just give each user two accounts" isn't a solution. Sure, it'll work
for existing released versions of PG, just like there's a lot of things
that people can do to hack around our deficiencies, but that doesn't
change that these are areas which we are lacking and where we should be
trying to provide a proper solution.

Thanks,

Stephen

#7Kenaniah Cerny
kenaniah@gmail.com
In reply to: Stephen Frost (#6)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Hi all,

Thank you for the feedback so far!

Attached is a completed implementation (including tests and documentation).
Based on the feedback I have received so far, I will be submitting this
implementation to the commitfest.

Thanks again,

Kenaniah

On Mon, Oct 11, 2021 at 9:05 AM Stephen Frost <sfrost@snowman.net> wrote:

Show quoted text

Greetings,

* David G. Johnston (david.g.johnston@gmail.com) wrote:

On Monday, October 11, 2021, Stephen Frost <sfrost@snowman.net> wrote:

I don't think "just don't grant access to those other databases"
is actually a proper answer- there is certainly a use-case for "I want
user X to have read access to all tables in *this* database, and also
allow them to connect to some other database but not have that same
level of access there."

Sure, that has a benefit. But creating a second user for the other
database and putting the onus on the user to use the correct credentials
when logging into a particular database is a valid option - it is in

fact

the status quo. Due to the complexity of adding a whole new grant
dimension to the system the status quo is an appealing option. Annoyance
factor aside it technically solves the per-database permissions problem

put

forth.

I disagree entirely that forcing users to have multiple accounts and to
deal with "using the correct one" is at all reasonable. That's an utter
hack that results in a given user having multiple different accounts-
something that gets really ugly to deal with in enterprise deployments
which use any kind of centralized authentication system.

No, that's not a solution. Perhaps there's another way to implement
this capability that is simpler than what's proposed here, but saying
"just give each user two accounts" isn't a solution. Sure, it'll work
for existing released versions of PG, just like there's a lot of things
that people can do to hack around our deficiencies, but that doesn't
change that these are areas which we are lacking and where we should be
trying to provide a proper solution.

Thanks,

Stephen

Attachments:

database-role-memberships-v2.patchapplication/octet-stream; name=database-role-memberships-v2.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fd6910ddbea1..6a2a41b9cf94 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1637,11 +1637,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>
@@ -1698,6 +1697,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..e5f7f000db73 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,31 @@ 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.
+  </para>
+
+  <note>
+    <para>
+     Database-specific role membership was introduced in PostgreSQL version 15.  In
+     earlier versions, role membership was always considered to be cluster-wide.  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>
+  </note>
+
+  <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 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 when the
+   recipient is connected to the same database that the grant was issued in.
   </para>
 
   <para>
@@ -270,6 +295,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 +420,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 afbf67c28cfa..9bf63457994c 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,8 +403,16 @@ RESET ROLE;
    <command>SET ROLE admin</command>.
   </para>
 
-  <para>
-  </para>
+  <warning>
+   <para>
+    There is a privilege escalation risk in granting database-specific
+    role membership in roles that have any of the <literal>SUPERUSER</literal>, <literal>CREATEDB</literal>,
+    or <literal>CREATEROLE</literal> attributes.  When connected to the specified database,
+    the <literal>SET ROLE</literal> command would allow for these special privileges to be
+    assumed by the grantee.  It is highly recommended that granting database-specific role
+    membership in roles with any of these special privileges be avoided.
+   </para>
+  </warning>
 
   <para>
    To destroy a group role, use <link
@@ -633,7 +646,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>
@@ -641,6 +654,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 aa7d4d5456b4..1a9fe4da50ce 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 029fab48df37..d6fcd5af2f7d 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"
@@ -918,6 +919,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 aa69821be496..b4eb2623a9c7 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);
@@ -833,11 +833,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);
 
 	/*
 	 * Close pg_authid, but keep lock till commit.
@@ -1052,7 +1052,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)))
@@ -1067,7 +1067,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)))
@@ -1257,6 +1257,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);
@@ -1291,11 +1302,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);
 	}
 
 	/*
@@ -1402,7 +1413,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;
@@ -1429,7 +1440,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\"",
@@ -1517,16 +1528,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;
 		}
@@ -1540,6 +1558,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))
 		{
@@ -1580,7 +1599,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;
@@ -1607,7 +1626,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\"",
@@ -1626,14 +1645,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;
 		}
 
@@ -1675,3 +1701,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 70e9e54d3e54..4786663e1f3f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3429,6 +3429,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 19eff201024f..c77af820a6ed 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 08f1bf1031c9..02a724d820eb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -340,7 +340,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
@@ -7157,6 +7157,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; }
@@ -7169,37 +7174,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 67f8b29434ac..ca51b21a886e 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,
@@ -4724,7 +4724,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;
@@ -4779,8 +4779,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
+		/* Find roles that memberid is directly a member of globally */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(InvalidOid),
 									  ObjectIdGetDatum(memberid));
 		for (i = 0; i < memlist->n_members; i++)
 		{
@@ -4789,7 +4790,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 			/*
 			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
+			 * OidIsValid() avoids crashing if that arises. This reports if
+			 * the admin option has been granted globally.
 			 */
 			if (otherid == admin_of &&
 				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
@@ -4805,6 +4807,35 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		}
 		ReleaseSysCacheList(memlist);
 
+		/* Find roles that memberid is directly a member of for the current database */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(MyDatabaseId),
+									  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 for a specific database.
+			 */
+			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);
+
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
 			roles_list = list_append_unique_oid(roles_list,
@@ -4855,7 +4886,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 +4912,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 +4948,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 +4959,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 +4999,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 +5076,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 4fbdc62d8c75..0fc161875c37 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 b54c91176699..8a4600505957 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4084,7 +4084,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 d6cb78dea8d8..d80a933ad18f 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -209,24 +209,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 c29101704a54..e60108bdc6f3 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 dumpGroups(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -583,7 +583,7 @@ main(int argc, char *argv[])
 
 			/* Dump role memberships --- need different method for pre-8.1 */
 			if (server_version >= 80100)
-				dumpRoleMembership(conn);
+				dumpRoleMembership(conn, "0");
 			else
 				dumpGroups(conn);
 		}
@@ -998,7 +998,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -1012,8 +1012,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)
@@ -1027,6 +1028,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");
 
@@ -1477,7 +1480,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");
@@ -1488,6 +1491,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;
 
@@ -1538,6 +1542,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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 3253b8751b13..892f0d2a370a 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202109101
+#define CATALOG_VERSION_NO	202110032
 
 #endif
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index b9d3ffd1c586..9c75eb496a51 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 3138877553f5..d3f35e5fff80 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2090,6 +2090,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 af771c901d1e..ac45de17daa8 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 d74a348600ca..853f4daf414e 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 1461e947cdf6..4b439692922c 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}
#8Asif Rehman
asifr.rehman@gmail.com
In reply to: Kenaniah Cerny (#7)
Re: Proposal: allow database-specific role memberships

The following review has been posted through the commitfest application:
make installcheck-world: not tested
Implements feature: not tested
Spec compliant: not tested
Documentation: not tested

The patch does not apply on HEAD anymore. Looks like it needs to be rebased.

The new status of this patch is: Waiting on Author

#9Kenaniah Cerny
kenaniah@gmail.com
In reply to: Asif Rehman (#8)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Thank you Asif. A rebased patch is attached.

On Thu, Oct 28, 2021 at 11:04 AM Asif Rehman <asifr.rehman@gmail.com> wrote:

Show quoted text

The following review has been posted through the commitfest application:
make installcheck-world: not tested
Implements feature: not tested
Spec compliant: not tested
Documentation: not tested

The patch does not apply on HEAD anymore. Looks like it needs to be
rebased.

The new status of this patch is: Waiting on Author

Attachments:

database-role-memberships-v3.patchapplication/octet-stream; name=database-role-memberships-v3.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..bccb0a081d 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 a897712de2..e5f7f000db 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,31 @@ 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.
+  </para>
+
+  <note>
+    <para>
+     Database-specific role membership was introduced in PostgreSQL version 15.  In
+     earlier versions, role membership was always considered to be cluster-wide.  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>
+  </note>
+
+  <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 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 when the
+   recipient is connected to the same database that the grant was issued in.
   </para>
 
   <para>
@@ -270,6 +295,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 +420,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 3014c864ea..b5013f0044 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 afbf67c28c..9bf6345799 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,8 +403,16 @@ RESET ROLE;
    <command>SET ROLE admin</command>.
   </para>
 
-  <para>
-  </para>
+  <warning>
+   <para>
+    There is a privilege escalation risk in granting database-specific
+    role membership in roles that have any of the <literal>SUPERUSER</literal>, <literal>CREATEDB</literal>,
+    or <literal>CREATEROLE</literal> attributes.  When connected to the specified database,
+    the <literal>SET ROLE</literal> command would allow for these special privileges to be
+    assumed by the grantee.  It is highly recommended that granting database-specific role
+    membership in roles with any of these special privileges be avoided.
+   </para>
+  </warning>
 
   <para>
    To destroy a group role, use <link
@@ -633,7 +646,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>
@@ -641,6 +654,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 aa7d4d5456..1a9fe4da50 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 029fab48df..d6fcd5af2f 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"
@@ -918,6 +919,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 aa69821be4..b4eb2623a9 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);
@@ -833,11 +833,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);
 
 	/*
 	 * Close pg_authid, but keep lock till commit.
@@ -1052,7 +1052,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)))
@@ -1067,7 +1067,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)))
@@ -1257,6 +1257,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);
@@ -1291,11 +1302,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);
 	}
 
 	/*
@@ -1402,7 +1413,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;
@@ -1429,7 +1440,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\"",
@@ -1517,16 +1528,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;
 		}
@@ -1540,6 +1558,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))
 		{
@@ -1580,7 +1599,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;
@@ -1607,7 +1626,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\"",
@@ -1626,14 +1645,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;
 		}
 
@@ -1675,3 +1701,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 82464c9889..983861c359 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3429,6 +3429,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 f537d3eb96..88e1cc10ce 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 d0eb80e69c..ae8448cefa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -346,7 +346,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
@@ -7164,6 +7164,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; }
@@ -7176,37 +7181,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 67f8b29434..ca51b21a88 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,
@@ -4724,7 +4724,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;
@@ -4779,8 +4779,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
+		/* Find roles that memberid is directly a member of globally */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(InvalidOid),
 									  ObjectIdGetDatum(memberid));
 		for (i = 0; i < memlist->n_members; i++)
 		{
@@ -4789,7 +4790,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 			/*
 			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
+			 * OidIsValid() avoids crashing if that arises. This reports if
+			 * the admin option has been granted globally.
 			 */
 			if (otherid == admin_of &&
 				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
@@ -4805,6 +4807,35 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		}
 		ReleaseSysCacheList(memlist);
 
+		/* Find roles that memberid is directly a member of for the current database */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(MyDatabaseId),
+									  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 for a specific database.
+			 */
+			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);
+
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
 			roles_list = list_append_unique_oid(roles_list,
@@ -4855,7 +4886,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 +4912,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 +4948,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 +4959,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 +4999,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 +5076,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 4fbdc62d8c..0fc161875c 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 9fa9e671a1..0835081f11 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4084,7 +4084,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 56870b46e4..61a8e6863e 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 c29101704a..e60108bdc6 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 dumpGroups(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -583,7 +583,7 @@ main(int argc, char *argv[])
 
 			/* Dump role memberships --- need different method for pre-8.1 */
 			if (server_version >= 80100)
-				dumpRoleMembership(conn);
+				dumpRoleMembership(conn, "0");
 			else
 				dumpGroups(conn);
 		}
@@ -998,7 +998,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -1012,8 +1012,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)
@@ -1027,6 +1028,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");
 
@@ -1477,7 +1480,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");
@@ -1488,6 +1491,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;
 
@@ -1538,6 +1542,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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 9faf017457..549e07df16 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202110272
+#define CATALOG_VERSION_NO	202110281
 
 #endif
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index b9d3ffd1c5..9c75eb496a 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 0b7a3cd65f..f5f8e4c000 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 49123e28a4..d03d3cf113 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2091,6 +2091,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 af771c901d..ac45de17da 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 c8cfbc30f6..6bd4814190 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 3ecf5fcfc5..6cf403afcd 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 0000000000..2ea9ba093d
--- /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 0000000000..66b6ec6bf7
--- /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 215eb899be..79fb69059b 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}
#10Daniel Gustafsson
daniel@yesql.se
In reply to: Kenaniah Cerny (#9)
Re: Proposal: allow database-specific role memberships

On 28 Oct 2021, at 21:39, Kenaniah Cerny <kenaniah@gmail.com> wrote:

Thank you Asif. A rebased patch is attached.

This patch fails to apply yet again, this time due to a collusion in
catversion.h. I think it's fine to omit the change in catversion.h as it's
likely to repeatedly cause conflicts, and instead just mention it on the
thread. Any committer picking it up will know to perform the change anyways,
so leaving it out can keep the patch from conflicting.

--
Daniel Gustafsson https://vmware.com/

#11Kenaniah Cerny
kenaniah@gmail.com
In reply to: Daniel Gustafsson (#10)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Thank you for the advice!

Attached is a rebased version of the patch that omits catversion.h in order
to avoid conflicts.

On Wed, Nov 17, 2021 at 6:17 AM Daniel Gustafsson <daniel@yesql.se> wrote:

Show quoted text

On 28 Oct 2021, at 21:39, Kenaniah Cerny <kenaniah@gmail.com> wrote:

Thank you Asif. A rebased patch is attached.

This patch fails to apply yet again, this time due to a collusion in
catversion.h. I think it's fine to omit the change in catversion.h as it's
likely to repeatedly cause conflicts, and instead just mention it on the
thread. Any committer picking it up will know to perform the change
anyways,
so leaving it out can keep the patch from conflicting.

--
Daniel Gustafsson https://vmware.com/

Attachments:

database-role-memberships-v4.patchapplication/octet-stream; name=database-role-memberships-v4.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f73..bccb0a081d1c 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..e5f7f000db73 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,31 @@ 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.
+  </para>
+
+  <note>
+    <para>
+     Database-specific role membership was introduced in PostgreSQL version 15.  In
+     earlier versions, role membership was always considered to be cluster-wide.  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>
+  </note>
+
+  <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 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 when the
+   recipient is connected to the same database that the grant was issued in.
   </para>
 
   <para>
@@ -270,6 +295,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 +420,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..fa2ed7a11afb 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,8 +403,16 @@ RESET ROLE;
    <command>SET ROLE admin</command>.
   </para>
 
-  <para>
-  </para>
+  <warning>
+   <para>
+    There is a privilege escalation risk in granting database-specific
+    role membership in roles that have any of the <literal>SUPERUSER</literal>, <literal>CREATEDB</literal>,
+    or <literal>CREATEROLE</literal> attributes.  When connected to the specified database,
+    the <literal>SET ROLE</literal> command would allow for these special privileges to be
+    assumed by the grantee.  It is highly recommended that granting database-specific role
+    membership in roles with any of these special privileges be avoided.
+   </para>
+  </warning>
 
   <para>
    To destroy a group role, use <link
@@ -639,7 +652,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 +660,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 aa7d4d5456b4..1a9fe4da50ce 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 029fab48df37..d6fcd5af2f7d 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"
@@ -918,6 +919,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 c8c0dd0dd536..2697c20d5405 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);
@@ -833,11 +833,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);
 
 	/*
 	 * Close pg_authid, but keep lock till commit.
@@ -1052,7 +1052,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)))
@@ -1067,7 +1067,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)))
@@ -1257,6 +1257,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)
 	{
@@ -1302,11 +1313,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);
 	}
 
 	/*
@@ -1413,7 +1424,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;
@@ -1440,7 +1451,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\"",
@@ -1528,16 +1539,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;
 		}
@@ -1551,6 +1569,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))
 		{
@@ -1591,7 +1610,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;
@@ -1618,7 +1637,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\"",
@@ -1637,14 +1656,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;
 		}
 
@@ -1686,3 +1712,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 297b6ee715f0..5c6e73a40372 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3432,6 +3432,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 f537d3eb968c..88e1cc10ceec 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 86ce33bd97aa..24be12e67630 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -346,7 +346,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
@@ -7164,6 +7164,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; }
@@ -7176,39 +7181,42 @@ 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->grantor = $5;
-					n->behavior = $6;
+					n->database = $5;
+					n->grantor = $6;
+					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->grantor = $8;
-					n->behavior = $9;
+					n->database = $8;
+					n->grantor = $9;
+					n->behavior = $10;
 					$$ = (Node*)n;
 				}
 		;
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434ac..ca51b21a886e 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,
@@ -4724,7 +4724,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;
@@ -4779,8 +4779,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
+		/* Find roles that memberid is directly a member of globally */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(InvalidOid),
 									  ObjectIdGetDatum(memberid));
 		for (i = 0; i < memlist->n_members; i++)
 		{
@@ -4789,7 +4790,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 			/*
 			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
+			 * OidIsValid() avoids crashing if that arises. This reports if
+			 * the admin option has been granted globally.
 			 */
 			if (otherid == admin_of &&
 				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
@@ -4805,6 +4807,35 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		}
 		ReleaseSysCacheList(memlist);
 
+		/* Find roles that memberid is directly a member of for the current database */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(MyDatabaseId),
+									  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 for a specific database.
+			 */
+			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);
+
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
 			roles_list = list_append_unique_oid(roles_list,
@@ -4855,7 +4886,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 +4912,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 +4948,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 +4959,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 +4999,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 +5076,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 4fbdc62d8c75..0fc161875c37 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 e1ea079e9e36..c1cc6c7a7873 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4084,7 +4084,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 56870b46e45c..61a8e6863e5e 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 c29101704a54..e60108bdc6f3 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 dumpGroups(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -583,7 +583,7 @@ main(int argc, char *argv[])
 
 			/* Dump role memberships --- need different method for pre-8.1 */
 			if (server_version >= 80100)
-				dumpRoleMembership(conn);
+				dumpRoleMembership(conn, "0");
 			else
 				dumpGroups(conn);
 		}
@@ -998,7 +998,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -1012,8 +1012,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)
@@ -1027,6 +1028,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");
 
@@ -1477,7 +1480,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");
@@ -1488,6 +1491,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;
 
@@ -1538,6 +1542,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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index b9d3ffd1c586..9c75eb496a51 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 067138e6b594..d5fe30507282 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2091,6 +2091,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 af771c901d1e..ac45de17daa8 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 c8cfbc30f605..6bd48141907f 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}
#12Julien Rouhaud
rjuju123@gmail.com
In reply to: Kenaniah Cerny (#11)
Re: Proposal: allow database-specific role memberships

Hi,

On Thu, Dec 2, 2021 at 2:26 AM Kenaniah Cerny <kenaniah@gmail.com> wrote:

Attached is a rebased version of the patch that omits catversion.h in order to avoid conflicts.

Unfortunately even without that the patch doesn't apply anymore
according to the cfbot: http://cfbot.cputube.org/patch_36_3374.log

1 out of 3 hunks FAILED -- saving rejects to file src/backend/parser/gram.y.rej
[...]
2 out of 8 hunks FAILED -- saving rejects to file
src/bin/pg_dump/pg_dumpall.c.rej

Could you send a rebased version?

In the meantime I'm switching the patch to Waiting on Author.

#13Kenaniah Cerny
kenaniah@gmail.com
In reply to: Julien Rouhaud (#12)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

The latest rebased version of the patch is attached.

On Tue, Jan 11, 2022 at 11:01 PM Julien Rouhaud <rjuju123@gmail.com> wrote:

Show quoted text

Hi,

On Thu, Dec 2, 2021 at 2:26 AM Kenaniah Cerny <kenaniah@gmail.com> wrote:

Attached is a rebased version of the patch that omits catversion.h in

order to avoid conflicts.

Unfortunately even without that the patch doesn't apply anymore
according to the cfbot: http://cfbot.cputube.org/patch_36_3374.log

1 out of 3 hunks FAILED -- saving rejects to file
src/backend/parser/gram.y.rej
[...]
2 out of 8 hunks FAILED -- saving rejects to file
src/bin/pg_dump/pg_dumpall.c.rej

Could you send a rebased version?

In the meantime I'm switching the patch to Waiting on Author.

Attachments:

database-role-memberships-v5.patchapplication/octet-stream; name=database-role-memberships-v5.patchDownload
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..e5f7f000db73 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,31 @@ 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.
+  </para>
+
+  <note>
+    <para>
+     Database-specific role membership was introduced in PostgreSQL version 15.  In
+     earlier versions, role membership was always considered to be cluster-wide.  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>
+  </note>
+
+  <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 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 when the
+   recipient is connected to the same database that the grant was issued in.
   </para>
 
   <para>
@@ -270,6 +295,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 +420,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..fa2ed7a11afb 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,8 +403,16 @@ RESET ROLE;
    <command>SET ROLE admin</command>.
   </para>
 
-  <para>
-  </para>
+  <warning>
+   <para>
+    There is a privilege escalation risk in granting database-specific
+    role membership in roles that have any of the <literal>SUPERUSER</literal>, <literal>CREATEDB</literal>,
+    or <literal>CREATEROLE</literal> attributes.  When connected to the specified database,
+    the <literal>SET ROLE</literal> command would allow for these special privileges to be
+    assumed by the grantee.  It is highly recommended that granting database-specific role
+    membership in roles with any of these special privileges be avoided.
+   </para>
+  </warning>
 
   <para>
    To destroy a group role, use <link
@@ -639,7 +652,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 +660,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..1528d2f1a805 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,
@@ -4724,7 +4724,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;
@@ -4779,8 +4779,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
 			continue;			/* ignore non-inheriting roles */
 
-		/* Find roles that memberid is directly a member of */
-		memlist = SearchSysCacheList1(AUTHMEMMEMROLE,
+		/* Find roles that memberid is directly a member of globally */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(InvalidOid),
 									  ObjectIdGetDatum(memberid));
 		for (i = 0; i < memlist->n_members; i++)
 		{
@@ -4789,7 +4790,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 			/*
 			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
+			 * OidIsValid() avoids crashing if that arises. This reports if
+			 * the admin option has been granted globally.
 			 */
 			if (otherid == admin_of &&
 				((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
@@ -4805,6 +4807,35 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 		}
 		ReleaseSysCacheList(memlist);
 
+		/* Find roles that memberid is directly a member of for the current database */
+		memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE,
+										ObjectIdGetDatum(MyDatabaseId),
+									  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 for a specific database.
+			 */
+			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);
+
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
 			roles_list = list_append_unique_oid(roles_list,
@@ -4855,7 +4886,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 +4912,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 +4948,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 +4959,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 +4999,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 +5076,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..32b4ce6637ec 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;
 
@@ -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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
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}
#14David G. Johnston
david.g.johnston@gmail.com
In reply to: Kenaniah Cerny (#13)
Re: Proposal: allow database-specific role memberships

On Fri, Jan 21, 2022 at 3:12 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

The latest rebased version of the patch is attached.

As I was just reminded, we tend to avoid specifying specific PostgreSQL
versions in our documentation. We just say what the current version does.
Here, the note sentences at lines 62 and 63 don't follow documentation
norms on that score and should just be removed. The last two sentences
belong in the main description body, not a note. Thus the whole note goes
away.

I don't think I really appreciated the value this feature would have when
combined with the predefined roles like pg_read_all_data and
pg_write_all_data.

I suppose I don't really appreciate the warning about SUPERUSER, etc...or
at least why this warning is somehow specific to the per-database version
of role membership. If this warning is desirable it should be worded to
apply to role membership in general - and possibly proposed as a separate
patch for consideration.

I didn't dive deeply but I think we now have at three places in the acl.c
code where after setting memlist from the system cache we perform nearly
identical for loops to generate the final roles_list. Possibly this needs
a refactor first so that you can introduce the per-database stuff more
succinctly. Basically, the vast majority of this commit is just adding
InvalidOid and databaseOid all other the place - with a few minor code
changes to accommodate the new arguments. The acl.c code should try and be
made done the same after post-refactor.

David J.

#15Kenaniah Cerny
kenaniah@gmail.com
In reply to: David G. Johnston (#14)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

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?

Kenaniah

On Fri, Jan 21, 2022 at 3:04 PM David G. Johnston <
david.g.johnston@gmail.com> wrote:

Show quoted text

On Fri, Jan 21, 2022 at 3:12 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

The latest rebased version of the patch is attached.

As I was just reminded, we tend to avoid specifying specific PostgreSQL
versions in our documentation. We just say what the current version does.
Here, the note sentences at lines 62 and 63 don't follow documentation
norms on that score and should just be removed. The last two sentences
belong in the main description body, not a note. Thus the whole note goes
away.

I don't think I really appreciated the value this feature would have when
combined with the predefined roles like pg_read_all_data and
pg_write_all_data.

I suppose I don't really appreciate the warning about SUPERUSER, etc...or
at least why this warning is somehow specific to the per-database version
of role membership. If this warning is desirable it should be worded to
apply to role membership in general - and possibly proposed as a separate
patch for consideration.

I didn't dive deeply but I think we now have at three places in the acl.c
code where after setting memlist from the system cache we perform nearly
identical for loops to generate the final roles_list. Possibly this needs
a refactor first so that you can introduce the per-database stuff more
succinctly. Basically, the vast majority of this commit is just adding
InvalidOid and databaseOid all other the place - with a few minor code
changes to accommodate the new arguments. The acl.c code should try and be
made done the same after post-refactor.

David J.

Attachments:

database-role-memberships-v5-alternate.patchapplication/octet-stream; name=database-role-memberships-v5-alternate.patchDownload
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..32b4ce6637ec 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;
 
@@ -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);
+
 		if (filename)
 		{
 			OPF = fopen(filename, PG_BINARY_A);
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}
#16Julien Rouhaud
rjuju123@gmail.com
In reply to: Kenaniah Cerny (#15)
Re: Proposal: allow database-specific role memberships

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?

#17Kenaniah Cerny
kenaniah@gmail.com
In reply to: Julien Rouhaud (#16)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

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 <rjuju123@gmail.com> wrote:

Show quoted text

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?

Attachments:

database-role-memberships-v6.patchapplication/x-patch; name=database-role-memberships-v6.patchDownload
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}
#18Julien Rouhaud
rjuju123@gmail.com
In reply to: Kenaniah Cerny (#17)
Re: Proposal: allow database-specific role memberships

Hi,

On Sat, Jan 22, 2022 at 05:28:05AM -0800, Kenaniah Cerny wrote:

Thank you so much for the backtrace!

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

Thanks! The cfbot turned green since:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/36/3374

#19Andres Freund
andres@anarazel.de
In reply to: Julien Rouhaud (#18)
Re: Proposal: allow database-specific role memberships

Hi,

On 2022-01-22 22:56:44 +0800, Julien Rouhaud wrote:

On Sat, Jan 22, 2022 at 05:28:05AM -0800, Kenaniah Cerny wrote:

Thank you so much for the backtrace!

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

Thanks! The cfbot turned green since:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/36/3374

red again: https://cirrus-ci.com/task/5516269981007872?logs=test_world#L1480

Marked as waiting-on-author.

- Andres

#20Kenaniah Cerny
kenaniah@gmail.com
In reply to: Andres Freund (#19)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Thanks Andres,

An updated patch is attached.

- Kenaniah

On Mon, Mar 21, 2022 at 5:40 PM Andres Freund <andres@anarazel.de> wrote:

Show quoted text

Hi,

On 2022-01-22 22:56:44 +0800, Julien Rouhaud wrote:

On Sat, Jan 22, 2022 at 05:28:05AM -0800, Kenaniah Cerny wrote:

Thank you so much for the backtrace!

This latest patch should address by moving the dumpRoleMembership call

to

before the pointer is closed.

Thanks! The cfbot turned green since:

https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/36/3374

red again:
https://cirrus-ci.com/task/5516269981007872?logs=test_world#L1480

Marked as waiting-on-author.

- Andres

Attachments:

database-role-memberships-v7.patchapplication/octet-stream; name=database-role-memberships-v7.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2a8cd0266491..03ab7d63ea1a 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 623e5ec77895..db2e8379799b 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -47,6 +47,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"
@@ -1108,6 +1109,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 d4f8455a2bdb..15b8af1d2923 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3438,6 +3438,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 f1002afe7a0d..94a9b485e134 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 0036c2f9e2d6..b8fa11b3fba8 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
@@ -7261,6 +7261,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; }
@@ -7273,37 +7278,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 ec073e1ed061..42351636d395 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,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 fbd11883e17b..0b7f138817c9 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4081,7 +4081,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 9c9f7c6d63c0..4f723d07be9c 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");
 
@@ -1265,7 +1268,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");
@@ -1276,6 +1279,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;
 
@@ -1316,6 +1320,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 2f618cb22926..1e7c7f4af351 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2094,6 +2094,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 91ce3d8e9c35..82a9a0451019 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -210,7 +210,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}
#21Kenaniah Cerny
kenaniah@gmail.com
In reply to: Kenaniah Cerny (#20)
Re: Proposal: allow database-specific role memberships

Hi all,

cfbot is once again green as of the v7 patch:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/37/3374

- Kenaniah

#22Greg Stark
stark@mit.edu
In reply to: Kenaniah Cerny (#21)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Patch doesn't apply again...

[image: 1jfj7m.jpg]

Attachments:

1jfj7m.jpgimage/jpeg; name=1jfj7m.jpgDownload
����JFIF���	

			

		


	����"��8	
���ea����it~���0�7C��lN��1��~��a<��5����P�wD2����������]Q+�[L���F����_gF�!�Q������4�l�z���u�.�|����m����m�EH�L���uE�n1�_�6��HnS,�!��6;��e���.�W[j�Y����Z�X�{>���N�������.��lQ�]9(����0����} �@��3\�^,=w�a�xD���?1 ��s��t����D1����zu��vr�x\uv2��h*�^0�,if�#�0�t$�����h>"�r�6�F��+1���U�r]�=��v�������7U�g�G��'�$[��>M��
*��SLr|�����%�����"�_���0o��W��q���VY\�t�����o�F~���F	;�2�V�!^|-�,��$�k��,X��������[S,����2xt�Po�)tX���v"<�{���K�YsR��;Y��>�W�O�7��he�H+��
���@J��l��H��z�����Oo�������]?��[J�o�����]qC����L������P&B��I�eaf+���=������\V��<���Q,��\�F�RF����5��/��9��s������bz�h�
�^�����=���zT�	`��t�#:�?�-�����sEt�.D��\��N?��*j��Xg���P�6�����{�>|�(JXi#�C���@G�`�ajUn�H�]�N���hS�	!+���T����ZkN1)��!��#�w�N�-Nk�5z���:�85Q������&��i���~~�����N��\~��*�5������y?�!�����36���ni��CfX��f��z��E�{��9<��&���c�A�.`)2���(�m9�1U�x��lMw�RW�rxg���<hG�tm�e5���@:����{��J�fkm�����i�v�� s�FF�wyMU�V��?��F��d�i�WC+Y�=��r��,["t�����(��L�3*L����-oE�|���a������K)U`$
����YZ������+�U�-�o\&�&���>�v�S��/IZX��}k�tI3��GJ��{6E�n,���b71;���%�g�1�v���AI���8�R��f?J�c,V�l��5zF�g�D�&QvS�,sa��
�.V[!��"��;9'~%���`\�������:5�����[��n�`��$���)�\7!/<lip�����������uS_������,��2Q�0�.����������rz�lj�������Gq�G����5�d�=p+\���@w��D��dQ�E�M�2�[dr9���T]�'e�\e�fz{�����Q�P�]p���]�h[6�I��o(N'M�-�wK5�����9w�����]Q�Y�n9�#������.t'#�v=�}�v�j��OT-�N������l8(�� �k5�er����N�f�����PU���j���H���Q��9��jr4�$���w
n0����=�[�B�1���Y~SKS��2����+���������G��k����;+��C(����]4�A:T��$K�V�v�l��^�����oS-��{z�Y#����}(�����Tm���ik��zQ�_
�:�n�7�lVBZH��L�{��x���|�y�s���.�sB��3w%��;�+��Wa�2H^�i��d����G��6�r���#Z4��3;�t�#�<�8F�:�ZD���U?�b>�R-�H�������#DEV[K.�F��ml�(6k���Dj<�,vioQ��G��9b��p�[�9�pB��6��@cg���5n��r���8���`
���t��a}�;�9,��I�15�UL���|�>�����G�Mk~�I��>;�aS���>����K�P���}��oP�G�����q�)�Z�	�D*yW���� 2g7L�.�i��"x����x�?�7�<7���c���j�#�w7o��.��%�f��m�r�(���`�Vw@�p%d�Y*�l�-0�\gj�,��n��H�6 �64���^�Gr�	j��[+e�]�N���+�O���=������I����H�Z�T"���c&k���D��7����K��W�S|�:N|���!*��D|�q��AZ��S���MYv��7FWId�;*v��^�E�n&����!�D5x�D�@3�R���K�#���'\�!b�J�)'N������[]<����'�=������\��A�^n�q(� [8B������<KT0��(���#iT�n����Y����)I�C+3�z��T�7s�����B���a�����HUmc��BQu�G�c��5��n_;���u��Q{����@o������)�����N�+}���#��Lb���DMx��.�os)��?�{���5!�D� ��]��^-=<�=���*����tB���4�du�@���KB�aR�c�
�����&�������S*QAz8(�9��qu�N���/]N�R,f8��.Zr���~�������}�5��nK�gK]S�A����1��z}j4�����F���)��&�\�{g�_!N��0�$��,�7�wB��<�H���2��+\���$b���S��:d6�<AIQ�/���dD(�Z���@������=U����Ue���~������11��we�:��r����sG���5�����J�-&�wb���f-w����W����W��je��t>����(_���FJ�eW��S������;l�{�Dc�w%����X�0�L�+O��I3gh�<u.w95����/!	S�l�)("�s/eM�����3got�M1E��*�??��eH��3���#��!<dt]LN�N:F�3����k�c��
�������~��W�#�V`����$VX�K
���*�i?$�����?�i�yz�	$)a�����R2@5~k���x��*��J���ni�����.�u��-��q9BX+�\P�%
�g|9�R���VP�zt;uW�C�����[�����'*$�u�������������,_c�q=�>z�]:You�{���N�#����`�##�$��j�$p�]��e|-�aO7a0�l!��[2Z��L/�8d������x?W���CL��I]_��?b�0�+)|y���������S��`U�U�F�CR5��������
�lv�>�s������JSa-~�Q����y]����cL.�.��:�^�<�[���L1�a����ERvCc1_�����y'm7x%��Oo���+�Y�0��{��LB���C���k��|.�Ug�60+K�/d[���1�LQ��|���Gc���k�����J�9�E��[B��o�T��1���@���K���Z����^�/X�{1�������=���L�c��=�9Z�&.��$Qy3
��<�������U���N�`�������`~I�I� Ui,�B_M� ,�Ar���Z������(;�^�j���*��E=Lh�El�_�zo�>�4��JI�X���G����	n@G�a�l�`r��ek~����a���J����{�t��Us��x5������]:[ko�{�3BZ�g�!�\�:���~�'�����,u������J&x�O��IVP�'����M�4+�}���i��r���y��u���)G%��}(Si%���}o5��a����S�uQ)W������CC#	]j
T����5��u�[�K�PV�G��q���|6ku���/������\�cms�{�s4�X[H���r����H�v��t�SAM^�,��]s�hH"�>�i�C�4�t<�\8|(�����>��r�
J����� ��F[�����
�:{hA.��Q��m����zCX�4+-�1<{(����}
k�����s+�~�.�2�-h_(Y���0�G?xj�~�Al��2��E�#�D�����7��J?�d�`��
�����D]�M�*}l&I��'I^&�l��mb�����qN�������1r���m���:=0�'��,7���Z��}guF��K�J�dD�H(� Hm�*��a*�����F�U��7
�����dt���������}�H���	��6�#R<�(���6T�eQ�3]1�g�;����A'�1IN�$kV��F����nv���(��R[����uo#����#f1>���uv�<A���,�M7�������mx�����>��C��Pf�����V�;.��p�����dc�BS������c�%f�,\Iv����WH�E��R�B�@��B�JvJgBYJ��E�h�8��=G��B��{W�
Acb�}��X�������H^���>w+V��~Z�����'��������"�:�+�z��%s��;��!�df��2���/��{���g��R���8��7����a�������@��a2&*�����%�6���E�.�;�f��h������;����2�
����T���@!"5#126 $4AU%3BT&DQRa0ESqCF�Vc��.��OPCi"��Ob'�.�������5��W����F+�e]|�����O���S_'�l���������?��i��*�62��&�mME<���5�,�(�S�i�c��jk���7�eMz\�������4�+?&6���'i�������T���������X��5�p�jk�`?�����m����6ve=����7<]Mz^���SA���9.���`����5�z�eMEL����X�$/�S��`?�����G��S^�^#SE��������*��j5��D��k?
�cj�(�fv�m]M$N�������v�u]|�����Q���W_'�m���b�����1�*k����u��O����>'�]MF����������4(�h_��:����
� &r-������S�?����pWf��L� c���<=AAT�l���:��?�h��l���������z^yQl��=��c~�WUU[��I1���ih����6�j��j|>����
^F����J<�_Q���~��`���c�K[r���l/�M��������u����B5���R���
���u�������_�}W�b�.
�\�������W��CJ|O��6�v�D|p�Xf�_N�����q������X��U�g�
��������k�~��_�������1�bui���N���7$��{����N�S4��t���G[��\sU~�S����Z[����q_�r����X�����Y�s�b�J�w=N�)�y�@�V�u�[�r��2�-��%mYu/����[j>j#�������o��}U���k����{	,�lh�����c�Hg������A��Gi�u�:�X_�3Y/�Z�J�=�����X��U�g�
��
�d��k]+��k���6�[���m���Q��O�Q�����?������Z����`+������������;&X��:9�S�����c������c��I�$�ic��5��-�S��?����Q����u�����]NR=S����w�X������2/�����PP�������2Yhv/�Q?�OY/�Z�K�8�c]U�������������z���%��Z�_��V_���_����������_���U�O�j���M�&6�S�c�Wmqi����R��/��u�?G~��1���6������������ec]C�������u�u��u1�����[kmm���:~O��1�>Z�o���n
����bR����]��w ��%��q�k~����F��"?�OD"c c" ������ 5Ps��}]f?PR���R���3q*���bM���I���"S�8cy���(��������������#���SO�^'�\������������X��T��������I0�z��>Z��x[�u��'���R�I1.�i�{ c����������C���HF����:�[kmq���[F���5���
��T��S��r��#�G�	m�b��1���}�W�L:,����i\Z"�h���l�"Q2s%:���K���|���������CY@<v��U)���Z#��G����m)�#}*��L�Y����Q����k��K��Uu"6X����}f�#*��Qr����p�g��E?�9�+�8�X���5�1�^u�"~����7�x�a�f���/���Bj���QN�Q�k��+QY��t��J����q�C���9��J�]S��c������[m��_hFN���=�Q�Q�D0h�RLg�t��~��Nh�h�qj�q���_��e��~V���R�v�T�1xJ����l>�n���Z����v�C�~��f�k+@1U������4Y��j���O%^�����������Z��z1�����l���H�
,Au6��r����Do5�Bq��4�_ �J��I�����Eo��4�L�;[���Z������(L��xk�z���z
���,[�4U�)��{�u��Z����~���qg_�4��f����!��Wn�V6�c�r���z��\G����
���s����@"��/������O���4,�|��|z��-��|��I�w�.����7W���%D���=���(I�r�
Y��-�\u�����B�*~P7�:������M�n��YBH5$�R[f�O!������o��H�-/����I��[�d"�u��=<Fh�l��T� �c��WY���/��u��/�������r}]�wm�o��`Z�b�|+]z3��~�mY�u�j�i&x��?}s�d/����E�}t��*<���W�#�6���fP��X���=b��)%B�3������N���I�\�X�L��U����|MY����W���5d��'������i�^����"P�	��u44�6_�PS�a�q�z����;t�8��c� sm}���*u��n�S��H����� &3�T�>�L)�I�l����p[WE����������qf�L���G��s�W����yp�K�+X%b�����AK�B�R��6�P����AG�3/�"�S�J$v�ug &��N�.����v�k���^�e?����$���D��O�Z�k:�~"��]<1$���:��X;�E�9k3�����,[XM:��.9���Is��F�=B�����M����Wq����_�75��u�t������T�:Nb�T�����a����m��| `|k)$��,�.�
�?hEN���iRq����)`)�#�Hk+H�L���0�jX�"�����=����������>�Uc�:��R��[b���_���f�0eVp*L6�CY;�c�/�n����vu�+�i�X��(��V!y*����
}�^��O�z������NHHZ���Y�������Q�y�_f���bK���z�rijgh�X��c���s?�h�"�Y��
��
������w����AB��L���C�[�V	W8�:�����_N����{5����U0���C�^d�VM<�4V!���E���6����
�k��������-u/R$�Z;B~���:���X�)�5���Zj����.�3�$�{\H$8��Iz�����t��1�g��W�������������W$��9z,�U�$�OS��]^\����i�O���k��.�fc�1�l��+�Z�����(�X�1���O�����
U��m���cDu�9]�pG������g�
/��+���df�D���j���=d��k]=�z������CE��
�J������h�bJu���W�����J�aeu���D�����y�al��*L�+^l�r��Ugu����Dkm�G��3��
�f=��"f�a%�4�����Kn5�{�Z����1`�*���!S�!Q�����Kk����_�>����K_S=g���U^=5����E,Z���� aP��g����X*�c5���PJ� �d���n���QQ�(fryFF�����N_a���B��/�'����XS�����?�^K���K������Z���J����!1������*(uS��������rR�f	�\��_!���t���4�]��+��K9#��[^�=�SSU������[l��PFX���$����E��6�WH ����%6�*5P���k�1e��?�+Wf����"�Y�civc��#��sc/��=��+jiH
�;I����M-�c���{�9��������q�����#�q����
�H{-w��O/��:�������x��c�N�
�"�>;����:-)\4���z3TC$0�~�
��U���BR,U*'��j6��]D�E���8�-���1��#�L�
���mDk�'/�J0�?!u��b�>H�9k&
��#���*$�����`gQ����&]<�H���. 1���<�nE��m����\-{�h�?� ���9�,��DK�#�w�II�F���u<�""w�2D{�<u��~�,K�6�O�}�}
?�>��U���Y����[�(v�O��r%�Q<��&"�����:�����t�8,�����%��X��C�yjL�J��]��I�XZ����-��.��{%�3���< ��X���]jc�dG�����F�����;��q
y���������9����j�\�"nb��#(U����L�����Oy�E��G�9����
7����}�h����G����3�^?���;��i���-7��;��8?p��07T��*�!��8���Ug�G[|<�g����>����vz�:1���|���	���P�t�&;������2����l�[�uY��k����6��4����5&j(���������F�S�����a�u�����bFQ��,�fr�X2��{���k�sq}���[k����g&��#E��P?����zs8�F�e6�L �l���Q|#���?�v'�:����0�-���x����`o��,[���L��R�������Q���K�o!%�y |�E� ���;�~,1��JGr2�{ ��?	����iCF��f��uffkv�B
8��X1��X�Z����Oi���t�y��`q�BV[(�cL$�k�3����R��@@�����-� �a���������K��v����rH��X&-�j�b�5����O����m�u��V���a:����&��dK'W�&A/�r��APG<���[r8*���}�	���r�����T�c<�@}���7�}����y����|����~�i��w�[��4���K�O8���B��(�L�G���<�{�n'�:b&&7��������k}m��_i?CF���=[�n�=��|��^J����" wg��
6�b;nt�����D�
-N��:���#;�����]x�1��%��������US&�a��SI"������dOjx�+Jd���C��C����:��������V3#O/Iw�3��|��o��r����c���g#*�Wb���
�����?�j��,:8m�A�\���ff�I(&"�\l0�)xp�Y�7>C: 9G���e�;���3�G���< ���I�bU;K6����@�^�e?�5,cB?��O�{Db�81�~������~���V���L����)���E������8�D���9Dh�b0�{��� ������#a�;��dt`#T{���2�R�{9#2���5��wrx�KA�Ja�����qN��T�c�)k�3(u{Q"�u��uP���(���:��?��U-[�[�P#��E��X�I0��#4q� ;f;�W���N�-�
��q���m���;���Q��	��'�]�
f>�:"@����6�yH���3T�)�`fcJ3)�1�
�:_r$;���o�V�L�^�B���q�#��(�k?������������k�h��������.Jj���)�^��T^�2���q�KN$M9�O��HXH�@��`Oe��(X�p�h�a-��jc�vl9=���@9�L�����g�a5��??��U��)a���6�a�u��(���D�_g=U��5�m���:��������Z�Uw�c��q�������_9���Xbb�D��dY�`L�H����Cr9G�
VA~(��������!y���\���L��#��!C��k/(���w�Lw�+�����f&�F~G�o3��%:����m���v�m�����|,�|���{���a}������D�^5+3�]��i>#YQ3T��t�3dE��M(X��Q!����>c�G.s��A�6�������ab��m���~_v���m���������k�
{c��	�U`k;��V'�n%&����G+o*��t��}Jf]�c;L�s,V��i�[+�����\��P�wB*���LJ���<�T�C�eRc�T��w?�0�I��Q�9������C`�_@�w&�4(���-��0�10g��{������r�_��ru�^��������v�_��g�Q������r!�Oz"���3?T��u�;���Ue!�'Kx4�����85�#����^��,�`"%(�/�3��R����l� Q�Ka�\l2S��z�A����d;NPUhXC�yU���^��� ��g�����:.�c�'��T� Z��|hGx���3�d���\���������XD L-����[J�c�q�kVeg��Wj(�Ez��4Myd��T�{�
)��8�����^���\bH�B���N��AG~�!-����;fy�����}����+O�f��s��%��&;��[ko���}��j����!��-x�}��Z��[C��y+�YJ�)�J���"Q����K����d��L� �5	D�T����&�l����r����"��� {@2&p�)�}I����:�����X��5`���T3ZP��g��	��rGn��gU����k����!�����&'>t�#**f������A���m3Y���z�=$j9����y�N�h:���x���"E���T�����&����������Z!�7)��v���-�
<��K�rPK�B�-B������m��������t�#��.�T�'0u��Z��6 � f21:��)�0����LT��;��rj�,��;p���/����k�����HX�`	�Z����}1i�������4�JE�$�B9����m�}7>f�N�g�e,X��38Ug4���b�&fu���Sh��\��n���(opt���%I��hF�A�
?��e�J+�j��M �I� &ItpK���v^��YX���s��D�����H ����
���z�L����H��������FV����,[w�-�m��5?���6����w���������5��0E�d�%�e�/z{Q��mN���.��@NG#J���&J������I��Dwbdf`�-�����3��pmNbY�M�f�/6�%��5���4�������I�0QB��}��'!J������R�� ����Nx;���0�z���)����w��������?��F�^9G-���r����I�X�m2@�X0K���q2���h3���2�Y� ��b3��l�%4|�
K��7��#�����]�n��3`��,���Wbw�x��~��j@F��PG.���)\;���hmIsZ��DS�P���������,���>���W s!���T�������	A��������n,�@W�=F���ER�,�Dv���-/���k{�������dfb���	��v��;#*���79`�=MG�U���&�Z#cbu�8i�^�K�r��{�|��Q���s����?
���q�uu
#���-���(��p_�����0�{�Z�����������m��q�r���(���0����7�k��dD���D�!��*������[jbs��>?j@G�T�.�Y����Mv	h	�!"klO-�Ee�����=�t���H��������V G}0�����g�����S��(S`�A����;��vVP�#b]~���Ks=*��v:��o�����[9o)���EJS����X�4l��ad��p����h��R�S�z�!~����q�A�P��>u�g\������o��w��7�/o��t�Ay!�K"9�6Wo�k���FR�	��*����I�%���2Cl6~���5�R���>������W=��S�������������S��2<�`���c������U���R��$k�)���H4���G
��E�!�`2#N. ��-	Z�y������)�I��0
�z�����U<YT��hv��{����Z`E�w'9zio~N)���%Q�Q##��f���6�tR�g�@G���V�D��j�-�1�OXa���K��SW13m��'m���5�m:��MO����)�t���Q�B�x�-��$'���~d�
N$$��S����6��IFz��*Y��U������c��0��(�Q�#�^�X�Q���#�2��
[������v~j_����Yg���4���S�$Dk����K���w���p���=%|�}�]n#{�G�(��J������`&�}�U�d���y/a6L����`o�JY�G�;�,(7OGy��^e���������f�eLV
Eu��aL�_�~�W�1b��uj�r�6 J�(�n�g����O	�9�x�(���,����p�x�1F�g��o�G����c��Hq�%����C��� �k���dX��n���u���j��6Bq�UJ�������!`���n����z�\�l?�������e����(�� t����e�Jv�y1�
�ko��������I��p:����mc]�X���m!J*�����n=iwb{v�qOy��-Ib��x1'c�K��+��CcY�3�K�1�����~�,�&�G���������"��b��1!�ml���:�oE���H�K����j�rU.-.S�=�efG��j�Q �vSd��1��P������J�1�(bZ�r���T�2�(�'���i8q�E�j��?�L��L�9_�{��<6@-���I�
�����=��5�+���>s%�WOa0c�7����]Gx���������z~�������Y�,W��k��f<j��8����V@+�r����$N���O�|~������U��3��r�X�7�*%\�|��������[��>S�������)HH�"ci����yL��(�`��0J8e��m��+���a�S�d�$
��
FgU����j�����c�v�Ck6b����r�!T�c�e��{��$95��v�+
e~��>Q��c�{q����v�����e��O�������c�Pa2%K�%7T����T�V��2u��s�{��v����k}o�_����!�1�DI��}�C�;���RW���<��q^�T��Q�92�3��}cq���$���
F�	���:�������j����:��NcE������J_/�l�.]���3�DF*�F;��E�!�^LDM���-����[5�~MK��r�D�F������&�W�UD��
V!5������-�>�����X�������U�`"x��wMQ����n+��[�?O�� �qsQ�������Y��	j���1�Y������B1Yp�N�E�?re����	�<S�/�+v#���(I�FCrL��;�k��
M��?{�>��-i�\M�M��/�o�=����dX*�Y�`��kaD������k��1��nH�@G���k�W���UO�S��gv��q�1c����;��	��O��� ���_%r�f�b�"���b�}J;Wv�E���RpzS���u�ut�	3�Ai����f�m���6*��������}�������U��3*���+`��xz��������AD0��$�o5T����K��4����"=�Y�m[��8�l�_$����������m�LfjNN��]�dOt%��95Q�W�m~,��"�br	���z� ��?�������q�}�Gg���h�K�R��X���eq���%.Fx�m������k�W��>L�gRf�?����8�><ijM��a�}AKv� �7��0��)���r`E�D�����Yq��z��&��CW&���1L��}�0
�����C�I�� H��Q�s������v0=�2�UPH��[��]�0/�����]���"Qv-A���2������I�R'������e�J���};-���WV��[��+�����i���x4r�8��,�8�W��
�{�&��y����.��F��Y���������^�?����.��[���.~�������%4Y�dlc�W����Hm?	���}�~����&�z�0a��9j��������5���(-8�,������,Ga��[Z�����w$����^�wal�����c���Mu����v����,�k}�r������l���\
���|o��D�H�q:N1��n�k��W��!���j��\Gz�0���4�������)u��(}��NJ5B-�;�1� ����r�|�0��g|��]�U�[�����_#���8.���?�2e ���=y�,�����D��%�����N)��oo����W9����V�=_����f��qb�����s��!�,\�g
�:�#c{��F�uW�&�"(���8�C_���y�n��Jm�U�	0���b��l�l-�����-��a�o�`�����&l�p�>Z��E`AB�=8[�j�6,�\�/!���T3_d�+�#�3� {��g���s������0�\��L��d��5@�X����?/�WPc{6����������@��uN6r�!�������_������8���s�I�r8f�q����<6��>��Uw�_A�u������_�������y
)d��UN���W����&;���U�s�������*o���X���!��d`��+WjZ��%Q��ky�c��0�����^�ScV��`x��s�6C��0'r��B�\�����|p�9'��3����B9=I�����l���P�R�[��AG
ewlrj�\��d�-���l\G��� ����l�$�F�rKn�B~uS!0���Xb�d3Q@L��:��Q8�K�QLT�7�������p���v�[�Pk��;j�:W�V���X*���YJ�?�x���^��QaK���K���}��O��������{?��^~j��k�����6�7S��6CBj����F�I��L�E��L��Qz���r	fG��G��g���������������0w%����]���U%]l�L,�����o���tr!r
���3cs���>"�I `q��E������w@G�K�iZW\m����6p����������YzKB!��{.�w����W�l6-�����LWg���FN5W���`�P�g�$���A_�m+8��k�\��;4��
9#����b����
+e2�E�q�pwzg�������N��)��C��n�m�����N�r���;����V�����j~?�����[���b���\�C�����������(f��eW����T%����
>�f�p����������\�082�8�u��X�L���{���f�i\�=�vq���d���B|������e����r�v��v�U��N��Q?�%����B[	�m�d�Z��I8�{\�I� ��W��<��0u�<�z�~�In+�e�����Q^NC�����M�u\�o�C�&b�q��<=���J��:��&�R�*�+O/2��
l����I��O�������	Y�[�m4��>�O�}����?��	�0~���X�����L���n[������?����z�����g�&}����}G����}�eW
��,~������4���;����[��}�u�](9����X��
b��pm���-��]S�X���e���.%2>cW0���O��o��������y�{u2��(�?��*��I����f�l|���2)\�
�?+�
<�h'��'%!`&Gm���X���0��,��L/�R.Bd���x�B"#�^��C<���{����h�������v�X������aV��5��*!��-�h��l����d��A���j���XKp�GS4#�V5�C�����tT�F>�����~��������X���Y��[:c���U�z��^i�1�}�<�a21����,�0R`��?I_�E[1�5��<����A��7��b&Wnd-`�U3P��G�1�4*Pe����t�9���c�_�����N��o�O!*�X�.����lk��L��4�zn��dSN�n���81'��^�b5���Th2�X�2��Y�L�'��31����}#�g�}�t��K��	���K���m�c��4�����=���<F�SY.�P	Z�E���uO�w&�&�mq�t�*!�&���;�U�C2};o��V�X���o�-�}'�Tq[�[Kq�V�����M�S>��B������
�B�d�-K#1;����r��2i��}+������u��<^:+���������?����v��=S���uL���V����Kf[���A���r �`��~�^y1V�Y���:�����8��Ylj������l��j���k�p���p�}�*�z��p]�_G��o���7�&>~���f�;�Z�8�I����)���t�T7�ZQ+\��z�xm��`��7KT|?�����z�/���3��u��n�N���u�e��[C2��>��)�;�����>jo��e+�53n���Ub����9l�J�SBt�C��Nh�g�u]�nV�J�������S��v�N�,�O�:����v��K8b���~���_j��k�:��Fg�3���3�kk�]�����u&r!��������Z���r��O�=a��xG����������cX��U�o�.?�G4�e��E������~9J��&H:���L>���mi=��������~�����G����<alN�_5^�%V&@��S�T���c4��S�i�������*]1{��0(!�t�k�LDB��Oj�W��(��)�Z'k��D��e�'K�Y(��td��5b�m$���k����w��,�;������?���������>k���gE��man
������-S�U
 ��2u&���t�GR���Qa�H����>��u��k����~��|3oku�;
�_���3�� U�S_�['��i������?���6�w��Z������~�];��u/�,���M�����#!�\�����7������n��?V�yX��T#N�P�
-Ew�����'\5�-�Z6hV$�`�uz����u@���N�����tpq���Ym<c����[��Y��y�+�=�|�D�O���������Q���������m�����d�eJ�d�K�����Ms!�qy�LUw������c��:�z�j�DMezz�F��0b�����R������x�G������E��>v�Y/�Z��~�(>���:.6���?�X���]S	�,`�����V����t�C��W�9�{2U!*8���4�W�-;�2���+N���c�-br������k�B���a�-����f,J�X������]�������.t�/\�5�D��r��[n3�����r�����'�~��|k���[���_?��VV�^&��A����	����j������I�o���j�\���N�w##�oQ���5u����Q��������l����2q:�����f�M���L�s��j����	�\���E���50��unH�-���[�mt����$��U��P*JU�NK'��������"8��s���	B�����h�u/�8�Q��T�_X��d���(D3�UfZ�}��Q����q�F�`8�q�[��AUi�z�&��"���M���i-uFW)I����Un��p
kW�e�����*�^��{5��y�]��<���Ul6���x��/��n��|��[���U���~������t��X���,c%��@�#jh�H��f\����uG~���DX��q���	�C*�����Y|o�_k)W�ti�L��"-k�o^�����XYVd��2
�U�r���/rz;/T�V�:8|�W)>��Z�d�U6Z��X��(�p@]?���=�J����y���v�pr���� K f�T�z����:{5���W}F.&'4�Y:n��(^��"�,~��S��c��s���$wf�Ge-@C)�u.����6X���Fg+��F�����f��5��I���]|�����AK1����\�3{�(����Fj6������-a3Az�hH���T'C���/��7mx�h\A����B��z���r��;��}}�~�����T��Q��F���.go�G��O���v�t�������c];�z���:Rq=6�FL�d�2_N������(�E���k:,�U��:n������F���n�k����:���hQd*�����gYQs4R�q�\VF2u��+���W���y��)�id9z7w5b�j$�X>!+B�(�����9�[	Gg��q���[��U����,`N�����G_���W'Z��U_(fj��c_��t���!��PG�[������33:���1:���]�/��R$�\/s2Y�Q`�1���Mx���>
T�W
Y������v��������]O����S����+�|������������Q}��s����&*�.7.�m��2�g%��Z�4!)dq�e[UHG�-M�8	N�Z���Y:���nt*����:��X[��f�1��g�����Lm?��9lBXXWI$3W�$��Z�X2�fw��-�b�,�I0;|��)��Iz":\���q��f��6�s���U(��r�'�������%�d��k]'!a��Zd��N�K����cy����q���^\���u}���.S��{?v�.�-1��q��m������R���FMr����Q�_�.k9���������wV���g�LzAE������Z���=u�����4Q��� ����|��\Y��F��k���c];�j����*�����`o�}:���8�������S����P�R�����b�&�-�[+q��J~7��u��R�4Sd[�R>���^��b����"^D��
z����t����C�����b���wR�KX�E�����^�=C�!�BAj��L��Lhg��
f5��I��K����J ����{7����7���<oR�8� 1`w��d���5�5��*�im@�.a(Z\,P�|2=��Q>�is�	����KBk/1�����h���I��3:�6�G��?���%����~�9���-�t��kk?F���UQ��O���NK���Y�������G���������(�f��%��Z�R�i�7��u�X����QV���f�\�m|>x�1��g������_��](0r#]A����m�������*� �@5���O���cX��U�o��b(�^f��,�2N���X��������JYk�jJ�����N���d/.&�v���)R�Z����Z�T}.���YY+�U�5w"�p�yZ��[�>y4�,�02\}�$��J���������Y������]G��Vg}y�{m��&D���g��h�"�/�:�1�_�|��]��Tv'��Q���H�q�f�Y��Pu!���7 ���A�N%	u����O�sH�������������/F�@��l�����q�M�E�Z��q{�1
�ak��Lq��j�w�5�v�����Z�����8��S=���h[�2���_F�|�r�T����P�V��q�_v�+V��� ���{?h���5�G��y�F_Z�A���s9�m�c�^�3���&��[[-A�AM�v������������G^j�JPfUs�DWR"�����[Yj�Q��z�	�����o/���8���R��\�������(��t�O�C����f1��-u�w4���f|��
��Q[BG��;*�.G��xf�-�#����{��dh���_����=�I�NA�!l��CR�G�1�K)�jF�������lek�D��6S����H�Xze��V�>���E�u����������9��3������g�~�Z�����H�TZ3�k�=���`�x�6�C
Z%�^Z��G�m������c	����J�ny%n��T�K���of��Q������<�z��v�}�F������.�Lv������V�
��s�,M��{m�����d\�������n�T�lU�8��#����8;����)j��_�$MJ���>d���j��d�m!����K�!��x�J;�:r?���ALQ���b�����s���#gs0��)J������t
�������:�w��r�;��]S��s:�?����@��2"���r� |����c��	����F��t7mB{0��W;��2�����Y���s�B����A�����o�Fwm�v�?�g>v��frK��v�ueE=��k������
V�&��'�9��u��uQ*K���Vz�5os}�)��&d��U���n���~��c�sS��������;�?�������oy��B^o&����-��������4����98G��S�fs(�_;S:m������c#p!vls�\����'w�3��/|�F]c"h��������I6=R������}���Vw*�U��:�<���j��>�+�����+.AsmGTgx�z���k'��(�g�q���L!1A"Qaq2B�������#0Rs� br��Ct����3S���c�4������
?����ht�������|�����Z�KW�j�-_%���5|��/���[��CW�o�����5|��/����CW�o��
�_���|�������o��
�_���|��/��E�;}���_!���v�/��E����Aj��C���Z�KW�j�-_%���|�����Z�KW���|��	��K.���p���������s�������I8'cx*&��.���gr�^3��N����-�R��k>{��l�qY�w,��b�E�3.�����'z�����F��cQ�2g���|AEm��VG�5��ZW�����K�V����#���hF�����4p!]�	����&M?k0{�����]Ag�qY�w/e�9s��/�Y���M�,�;�,��S�	��jhGwpF;{�N0���a��O�rgL���y�&{��G�e��pr'r��#;�!��F��D�(!ja�G�V��������_��
~�f�QS��\���s�,���x���fY�������0�/�Y��2�xFN0����{�N0���a��O�r�P��nL���=������^�q	��k�V������A�L����\e�d��#����N�����+E�-s	�
����
}�.{����:)#�,��+2����g 1���r����+|'�&��d���7���������'/g~L�����'�k�����9�a�9� 5����r����+O��q��vN?�?0����������F�(�*�gh7���`�t.H������8�p	���7a%�gr�^3����%2���d�\V|�	�U�V��d���7���������','�����E6���b}��6u�@H9w��k�������+O��q��]����?0��e6�w���P�������6��Ct�U@�JYw����7��w������ ��M�km
��9+�������o�\i�d<:(n�h�;p�EF���E�p�W������+jA����v��D�I^�>K�r�c�~1�����p�t'>��������'F���5����P��b:��|Ny�X�{�.:�4`2h_1������P�=Y��=G�������1�����t]'Y��~!�����j�/�=�����+7nm5��U�����i�����������'^��7�R�����6��<�o�p+��D"����	�gLz#�����]��D���>����7n�p�	��3G:�EHZ��&7H9�A�n���g
������'��5�����oN�Oq����fY������s��J����N���-3�����[6�M�JS��l�^	�KY�?TA=_�B��yy��^���oo��Oe�9!Ds&A��u����d'8�N����I�H&A�U�q�Y�v���G��
�=H�G�y�f�U���A�����>�o��yg����-����c�8X�/�*�|A�Vm������s]�6m��\���f?t.0����)�T�d
��x�H���F��5-�.��G5Cp{w��n-X;�N�E�aQb��^K3��������B ��������^��r<�&���U\_�Dii���Es��N�����2���=����L�z�t,�6J����K�8soO��M��!��<;��)�L$�m�����oZc��GsOZd��wBkfG��6��x#�K��B���
����|&���^��r6;�g6����9����D�:��sOG�Q^'�ki>��+�k��TYk��Kfg������=�=�i���l��^3�g0�M'��q0�'�FDN�$��z%�2?�f_����,��)�������j�2konF�{:��9�Sw��������F��E��?z�Q�a��@p�H�B�f�]�J�'������"ent���!����bL���fY�����|BGC��k����1��`{�b��;��E���+4��.km[�,%�#x[�r1�>���	�u+2�2^��rDqy��y�P�8N����Y��-/^��<ME�W�Na`�h��]~�<8Z�C`oR�9����|F���)Zn�U/����vO���6g""��7�6��`�x��Um��<THV�;��p�i
br!Z �EE�d��M�4I�W��|@L����Z����T!��z�+6n��P�H:`k�=� {.����Z��I�����":T6�s��u&>�����}T�l�JK�2����p�gj�}��.�X���r���7u�u��N����eC5�]��}TG��W	����MW��-2�6d�Z�9X��E����/��.l6�,6�s_/$hM���Cx�7bV�����9�[���>�o�����,=��vl|[��j_[U)�(�>��J���VlY������6�
���l_)�QD �i}������.nq���C}/
��W�}���6�K���=����!��QVR��*�% �(�J@�'%P4|����v��*o@�������9�s���vG����u��)��9��������$0����L���tu��T���(�2�eruC���{����c�Mm�;��#���e������hu��~�b�m��d�rQk}���7"���5�Y���5�t��Uv����]%t�z��a*!z����`�*��)#�]z�Bc�������"����������>��h����7:�.~���.�E99�{�vo�\5�!G��b�lv(������3���!���-i,R��������1>vP&���h�I������t����W�Y�D���n�B$���j�,�h:v�
����#Ut��&����d�+��nSC��U��x�|.�������hD6Fv7u�e�]��F�7'�����$�1���a����*@������J	�ZjK.)��y�Twh�<�gQ��`�����gb��
���x���1������E1���x�G!P������A��_��B������n�m��-mT �^���-�>J��5]5�U4��h�j������1z����L�u�6�O������>4��=����Bp�v�&��m%�����`�m�'?4��"�l�WRv�F[N�1��=���u��
��	��7L(��d�7�����1#j�-V�[���9Df����:�C�bx���Z������U��Xm>����4����jP��4�����L���Uz"+T)��]�k���pS�t�0UWLS�0��g��M��'�F-��Nq�Q5�3Q^\��:@�c?D���j���2���^�0�2P���L���4�� �����f�)�X��<��#�R�|�
����^�K��4��w���������SxK��0`�t����=Z^�.L�t�!���	xi��1^���(�h��$���/B����n�(jN��`�zORugt��f�ek�O��m�; ���_�zlCbG�&{'J�Z(�c��;B����:Mo�v�!��~�<�+�(dI����p�i�;��6���X�$\fg��S�l�
k��8� ���=��7��9j����l�g.�j���&���* c�����F��w�D��N��	H�Q_1I�Vy�c0IL���8L.���N�W#�S�X����>��x�i��R���m�;#�l[.o(;8��,�/L�;���v���\
M�a�0G��j#'���a������������5��������$3Mg0M9'�)[��n�(���9'hB�8��a$��}�B�v�;��t�!���r)����4���F�xmv��M�`i
���-mS�1��tdf�#�D2����*��+X�L���t�j�2
W�:���8���A�x�|���~)Drq��6U�{-B��D5�?	�P�������iQp�x�	�R���zB������M���[ ��W~��l����g9�8��
�����	�l��m����8�?E[����g*�IsO)����7�S�@��PS�j������`�]�����+&��i����������@�^w'��$��(��'QE9D���H�z���?u���vN=��{�p��4��>��9�7�:���M[,�h���C���	�.YY�����Rbg�������;F$����D�����ab�o�Q*���P����y��Q�:$0M���f�6[T =�����h�\��y8�=j��(�����oj���!6����eDF�979�e��"�M�.E�n*]k`�&���t��T���dM0Y���������~7&\��9�����@�����7W�=��v�z�7�X���P���:ZC����&��T�2�b�����Zd���r�Li�%B������y��lM�6��S�7bl@��^4P��v������	2~��[�t��6XU��U�������L^�8kL �F���-a7GJ2p��3��f�j������0���0J�'�����|��P��V����4[����Z�m�Tg4��v��F}�G{�"6�oE���
#�6N�/��,�;���	�����Q�rP���7�Q�Sf�8E�����+]u��v��j���6��Y��K2���������1��$.S�%��������gX��)����c��rl�����RT�E(����@�e�������;#�hG���9	X���	��Cw��J����D6<
Yu�B!l�����C�rh]�u��"���@<��/���_5my�San�	�����Kb�\7��3���IZa�����{8F��J���o���'WG�e��{Is@�8`�C-)�����rMmR����TE��T�'z��m��h����!�~���D�M��so�>��7�����S������p�M5����:��q�~�E�>���b���ku�����JbH�_��=#�a�`�!���y�7�"��[�o���?r�M���U�k^��.�hP�\d���{�:6(�$�"�#x�S��[�5�p���4Fg[���hu��l�V�����55iE�t��ZBh6��S�
�d�^�����M�ud��7�����3��z�������9��Q[���m���1�)����Zt"m������3?$�[��%�w�������l�J�}���2�g)���	�"�i7�"���bVH�yAE�����M��I> �m�LnpC&���Q��,�, H:]
#��NI�i`�.Xu����v	�������5�Va
����Nt�5�t�;�2��Z����S�`N2�i�z��%��S�p�C����m�?#�|{S��M���m1�	�w)����I�0y���M��3�OE�h��C�8+O���-J@�x���I���h��b�kE4}��'�6�P�;���i���p�PZ��8�Z'I���
�����������Ti�5�0;���e���D	��A%>~�#��>r9M�����'C>d��H��a<�v��lR���'�H�(
9�<�o��(`1�����������9�GZ��,�?E�|����e�����q�Xk5O�D��s\���c�F�$~�
��7�n�����2r�
t�I>m�.��2�B$@����)�8&�T\��
���
��D&L��v�D,�5�����������8��6��"���S��d�&�m:X&�I����.��h;���e�F�X�r�NG��������d��������G���B�`����S��'��-\(�
�J��Q,�6\���x�|���G����Im!r��
\��Y�� �M6�F�"�N�ik�t�C����2�<V���
�>��|���sv�� ��l
��wj����E�b5�5�B��q���e\:
q��x��9�s���@�I]q�HV��w`Q[h7x�:����c���h��:��X�
�_��b>���v�ji�P����v������n�Nt��='�]
P��`�����-�����?'��S�B.6��4L
h��	����d���5�9
i�]U
��Z \��*�)���_�7&�Mm�
H����@��������m�����1Ts4z$�bVj5 "Eg��i��kE2C7�K�V�����7�i�B�m��/�=
0����R�J8��\�J�F����i>���l8!��C���a���d���]*.P�S\1��?s��~N?�?0�^�8a�:W'#k����,S4����t"kr��a!qS&�=��I��ql�$�c51B_h�a��� ����fM��'�n8
Gz��}������P�nNL�?���#;��f�^��x�P\]?��ori!�9�-"���0�������>���'�?4�PYb�Nc��k`�|n;T7Z�����l�$��o���B4��;!Nq���2�u&_[������}Q�j���dIV�E�	���:G���tm;}&7(n.h�!8�C.�a�\qLqo���a�&��5������1
��(:�D�S������-1��L�X��M!��V�<�p���B"$'Z���'7�	��	�(o<�E��_i�A����m�=Q�.�z�����G�u+��A��MO����?'����`cT�UO����?uB�����B` �4���g���AsK%}��J~^h�CN%F�`��:T<�G
���6�7`Nqn��U7(9��H�)��'6<�l���rp��f��s�L"�(�I�I4������4���3�y���;F �
�����M'��a��bFq���ued��h��p�6��������~?�9
��"Y�P�5�QG�h��T�����\�-�k���T�I��HK8/�*u"m��zcmS��8���P��-�:$���9��N�{=P���(�!�i��� ��	�p��e�t��H��&�S_�B��b������"6���{��Lm������*v�s�ia��0�sTHmx2����iA{k9��&�����	�.�P��n��]�5�r��g!����|����4�"T"�6+N���+�F*����s_)����S\Z��$����n����WQ8�R�bm�T9S����j$��[�lM��0���qv��iQ)kA0��5!Y_6�Jt):�q�m������N���D!���8(D���`��~�`�TL�i���8T*��j ���c�������:��8�	�:��NuEF��_��?u�[�~N>�����Q�#f��t!���X\(��)�����#�&&���H:�g�������T�[�P�%�R�6�/D=��U5�C�y2(��
��S�j��N�ip���7�$p�`~.�`��7MD�X���%�2N��V��S���pR=���<��C��v��7��)��
'�����\��aMh���|��['}�o_��"�s��HW�?���
.��9��j8G���/0��=�8u�[�~Gp���NuW�5���^�< \6��Q[a��~��6� 7�ji�pVt��Nm��x�M�A��)��Gs���)8�%!���#5Q��KUN �F!2?
��D�},eD��FX��t��Kb�b��w�Vf����a���X����U?SR��92���'�#O4��)���M�Vh7K�
�'��&�C��D�)�S��Z��B��.��9��'8���;�j�n'w����?#x{�������Y�4SZi�F>�i��QCmTX8�S��^Xu8��d��dK�"�
�%�tu("�i�&rN��zt��u���DA�)�9#�?f���\�*jP�{�.~ls��hY���N��&��J+��M4��t�N��jQZD��T��O�C�)v����5�(0����&��Io)�A�F��LK�jM30��������^3��y���F���O����?#?hG!��9B����y�Lhg}��!�!u�
����xv	��F��~�������;S��n����c�]�A�F�j�Z"W�R|6���F����ut���.l����#s�������V�����D�J��Z�����c=.D���
�6H���)Z"W�Rkl�����w�cI0�g�W����.~�w��s��k��Ux7	^�����l�JTp��5��������-
�m���t�:���o!�������e�����;v�}�o���W���L�sWt����o�M�Ln����p���H�N�D�����9�5��m���"a���hxu��-Y�w/g�98<Y4���N�+Z�Jic�k����tA���C��	��&����?�d���|AfY���q�/������Sg�+4������Y�4:��Ck{��2Q���2>�b-��g�L���J���2$8��N����1�_s�[�~N?���
����,#^��f�jf.��u'�`umO��%��2����g'	�l����<%�TF��?��:s)I���Y��'��6����Y4?�h���,�8�o.sM������1D�w�����8�6��=�S�oW��Be�y&�4���|�h�H�P�4:~������~?�;(�s������l�r��W��Va�Ru�C����,�;�����s�6M��Zi�n�������,j������k'�d|�6r���L�V��<�s�u��"B�ZmL�����l8��ELM<��-FC�`���X�229�'�����&JFS�+X�O�p~
�%��jc����������}����Z"�
b������J��N��<P��P����]����B����T�����g�M�@�2���6�~O�Y&��Z> �a�"��\����,r���{Z�6����;�1P�D~�����C��������/<�(�X�N�f:��M�O���z�D��dk�t��}�o��8��?hUpW�hCZs����M�����)�=������:����2;/A�:�Fi�3�%�t+$�]�%q�WR��hk������erp�>��m�'-]��49�gC�be^�h�q?D�X25��*���y*vt]ehM���T���+.i����[@�O���0'9u��Fn���j�n�k�5�q,����zh���#��������e��#� ��V*�qB�/q�[�~N?�?0��'-��;���p�E���u�:n�@��z-�P^�	�@�<���0�@��$�i��g�k������kO�w���[.���t�\��N��-4�m�oOm��k��:�D�dj�q�P�X������6��x���
��Z�M����\*#X�W�Z�&�h�������E���)�o�L&�����.Nd����`�Wm�zk�H9����v�����������d��asd���;�
�pu1gWr���Y����m7����k���T������+�,_
�oG���������?j�?�d��Z> ��EM����/�37�f_���'^���J����M}��}G�;���Y�w"-J�4M�m�R��6�d��Y��:�)��+Z$w��d��7J���e����^���%9o�p�*4XQH�jY �)�m1u���ak�W�@���������=T6�"�=~h�3#]���)��}�:��q�����\��mr%�C����-/B��E��$W	��r{��H�Y�
�m1
�J������bB�����,�M`n�:�������D����G]�2E>��X��2�|AC�t�d��E�^�ly���~�P���A0�i�md��0�/�Z-��H?��a:]K�0��|�����D����+�	���8�^�i��5;�hOX��M7���SN�KRoB���OJ�;6N�Dy����1��\��L�hr����+�y!��,������
�=���D�A������fY���Sh�SN�R�#��Tj�Fu���k^�x���#��P�HN-2h��hy�J��2Q[1##"���N�VOi�2��!#u���O<��]���p�)� �:�m�g	�����td2h� �F�(�R���V��>�����~�rK���l����K�T;v���I�*'<Z�	�l��!�
W�����r����,�B�7Y�x9 LZ
@���[�5�oqZ��|A��Y"�s�m�^����m���,�;������kf9St�e����Y��Nx3��uY�w&��U�d'���%~�"�,�xu
$�9P����$)���7^���T�z3�nl��B�x98��w���R����_�����hL���)$s�l�,��_���$;-u�~���n�L�"J���b���M�>��_-�E�	�������|�z(0�����ajy�$5<����Y�Bn���2�����U���[�h��l����:���|A2Z�a��r�^�iM~z/��E�4-Y��z���C��Vt���w�so�����	���[��������%�D��[���M�)�V�pw�v f0��*v���U��zT���K�>��������
�kd�
ia=����e!&/����w�+��0���	���G7	������D�nR��?c�0��7���@p4��{P�o�B�>l�1�����B�������!�!��a���m���+M�F�nq���q8DX�~n �k��v���
K-|��ms�78�Z��7�����a������4�������a��@_�mT��5f��Bu�)�Q-�5"e4�� ���z*[�����.(�<Z�VYp
��^K���8n����1����ge����Z�F������g���j4{U���g\%v���?���-�=��5�WaP}a�n�!�
���#d��o9��V���I�@�C�K�''��V��6s�vX.h��bc�5��-:�����N��ZS�}�}E=�}�o��L�O��f�Ew$#�o�?�L�B��f��|�z"g��=���'���R�Z|����}v�D
=�=z
�_�'�;A�}�R���Hm�zm�AC7D&]D��a��)����+A�O�m���`�
�[�M(����J���������B����+��g��m�G�7�+
�����'!1AQaq��������� ��?!�`�e_,�E
g]F���!^�,|����ML���O�!n��I�C����~��8�Fr���C3-o��
��o��9�[�yn�����f8���k�'#
(��s6y+Q�6���������&�lQ���Ezjr��r�@��2�>�����>�����������~�i�������p��jP��N�k��H�O��Q��d�|'����=rkg����'�����}"%�����]@�p/��SJXU
�c#�@�\0���M�O����	�C�FY���������5����y�}1P���2�~F�z�v����g0��6Bj�7�����Y]�������~��xUG�����m���c;�{0����g�Pa��<����*��J�	Y�V����
	S�������q��]+B�C�|��D�:���S��;���#
B���1I�~3G��I�+p��Y�}�`�Z�����D">1���(U�(L����hPD�:&wx7�}��������t����}�:+�Ty��:��Kc�Q����91�L����f^Y�-���]�/Q�Y2������*vN�,��TN�3M��n���p��.}c�Mi���X��U�8���l����O~�.���W�}�v�������>���/,��A���q����7_y�&��s8=��+�/BYlgo<���|v@�7�K����[�1�=���E��ih���=I��o�r��=�w�z��.��_����S���iW�'E;@�e�9�v)]f�)V���:m`���M���7(����/���C�ha�\������[�>��EL����3����%������K�+a��2l}O�����8�9�������]U��h �M�R��i4y~<���{�����AUr��r���1S<2����$��d��p��tiV�r�E������p�]��W^�0��i��2�
y(v�D�}�tX5���P��`P,��K�"
��`�,�*}�L*��J'�t�b�0�`�U�Dx�|e��e��E��3�
�B���ap���Z�����fk������V�y�~�!��J>���������b�Q���������b>�+�r��
k�/���`�i�.���aL�<���TC�R�,�F�$��i=����]9G
��/�1$���!*
����� L=��X��,�VM���Y%3�lb�`�f��3IS�4Yqg�DD�k1�!\���*��hyH�j[���n���b����A���d0%�v���
�.��k^����O	,��y��q|�
9��f���t�?�������(X,�_�=H`��s�2���iP�I�~�x���K#��=	K��$���xd�gN������[�|���iB�:�:E��B�J�w2F�(U��2��� ��']4�Y�i,��:B$`A�b(�M����3���l-H��s
��5�����&�hM��(m'��{����/���_�:���Q��y�L%��R�Z�6<_� �:5t��PX�z��b1�C�U4�����h�V+GH��Y.����%�]�@���A)c��(�Auc�6���K�W~��oY���.�g��1��y��XC�G_pFX��Ry��
���d�;�����J��@�5L;my�~ ����D�i�M�q]�������;�}
���Dpd����]�(���y��|�:�^��n��>�` ��3�Lm!x�g�.2�~=��O���QF;����x�����oU(���I����t�2z�+�n_���ka:dasJ�y��/�^x�Uhi�l��+�������ja$z����t���7*�$a�qZ[�c�E�"l^;��B:]{d�u���]�`O��������(�[���4�3�~��
�V��K��=2��z����zn8Qd���`���D��Yc2�W����W0I���0��,���!��=6�
M>c��DX��A�K����wN�*���G�$c
:�=���@6'Rd�^���~��D^I�6��V�|��QR�y@���#��QcsL�g���`9���P��'���0���?)Y9iV�XE�M�P��5`%l��v�m�
!e�������t�����_f���F
����������@�-�0X�>�m�H=��m���C�Na���>�f���>��jm5������g���1��m�s�r�����)�K���m��G����J��~f.1�������o���x�dz��������tuC���-��}�	@2lv`�]���h�mj�*A��:&,����������
?P��_����k��U�. �o��R�<P��]��\?t[����
�p�GB
���
P��MAm�!Yk)���>��}_��mc��pZ
���'��ad����{�z�c�6�����
�w������q�P�P����s�Mhe�c>W}#��!1�X�4�v���.���]�uW�W^Y��s�2����v;J��5������G�-o�Vp��g���Vf���
�*_,�r!�4?L��W@\�<��o
��9/4�)<pG�l���w~�Xp�4nvZ���'F�8L[�?��C��@��o�V����.���?���M���~���F��nYG?8~�'^�..M'#4<,S�����'�,�]y?����et�p�_s�a��)����b�$e�;����A4J5�������/��Q������������5I*L:���������d�~%	��}E���Y�������*�H	y�F��bI�K<�.��8�.�&��*S�����5�2��|�FX�������������V����X��R@ �\�k}��M&���Yw���"�RY9��`����H�("'�1~]�,Z���%�����TX���)���1Ab�����e��%�1�N��0UK���T�f�^�+
��M^J�V���O8��n��k��y��3������V��s���s%
��f������Wu��%R�m����T��M$H?���������������L#D���}�q)n����G�_D�A�EA����8�;=K�����uv���_�*`"����������1�`�����
j������0G*����r@�R'0\\���SvB!�|���H�J�.K�y��	
Q����U����si	s#3�p�ib�n��K�:���5���������_j"��.���m`Z��"�y[b���Kh����u&EEm�Ec��H��yu6��B��
�J��
��Wr�[%���X��rel8�T���������J��'v�~�J]K�����D0e�r�#�GE���v�i05X
g�bM��P����� ��JrS���y
7�4A��FU(C-�`��-W(�
��������~��m�k&���:����n���Y (g(��:��d�n���+D�k�%�dKZ%L��WP�M���m�d��=�qc���kazd.�2n����������u�]e��W���k��#,��
4o~}�
� ���i��jP�"&�Gc�lb-�aq[�ca�J*����Y�)��l0�v|L��]�sDkO����kG�R ���:	

�j�Q��J�j���[x#u�m�X�Q5�R���s�)�.�;4ug��/u�WLEh4
�z���:-�4��H ��������~����5��^��b[ghT�nb���^Y����:���w3����Cm+��Y&�������@��e�DG���-)�,=Q�Q��"f�S���"�_�3���X�e��u)�
�l�\�"�F9\8����pJ_�.\���x��!�PZ6;�����������Q�3o�QK4�S���D#65o�\�V�pk�4�t��`�e{UWnF#�R��h�5�8�,7�Cx���W{.��Q���9��E�������B0�4r�l���[�gH��Y�%�gQ���+$�o���^���h����wm���#/��U�Y�p|<�X��Uw_8 ^h�B���1�_g��d~�g���a�ok���J����W�X#��96��V/�V�Yi��;���[V�Y3Z�\�*���
$�)> ���� y.7�h[y�
�&����������b�1���%���L�Ya]����>r��]��z�:f�T_'��<*�(�����5�go=3��L-lmb�O_yQDDU�E_la	{qk�C���������y`=�T����5�RU�N.���
��R��x���x���q����m"��W$�����@(����%�fG�J������h�E���7��U�����U�M"'�;1�i��Ng�F���1���t�����`h.=���X�����_�:����c�mzB��
�� �����L:��� ��J(�)�/�@�SZ�m�������^��/��x����2�$I�Q0q<>��P�E�"�T���|����
h���.M�����#L�]B��"�3����+���EP��c��x������d@��AZ
!n�<Y�%���'
�+awK��^���Eri�_Y�J;nUhp�C������������e�/.�Y(���� 6,�����,��[&_si[T
��(TP9>�|�;AB�O��1���="{��a����
p��aL|����[����8�|sn"�5�W�����q����w5��z��h�l�����-�
����������%�`���tTv ��q�h. z�|\8�K��tb���e<��Fn�G3n���2��/%�1�*H4������(����Zv�r1�*��o#U���s����������h���'�Yof&q�n��cr�0��7��1�S
��������!sW�>�:��O`j�^���?�gL3�5����������Z���#A������H��:w����	Q���X�J�w+�"���VF����R((<��lV����fy�������|���VU}a��LL���S!EN��Vk}�1�@��/}���u��r�����/��:��a�	��k?��!���!-��;��r��k!����5�QE{�����/�����hp<sPhLz6������-���!�R�r3[��t�iH�-���8�v��[[v�����$������-������9H�_�r������YKn�@��2����(����R�@\_7��#+HZ�W��`wn!iA�:%a0EP��f���5Ea�a�:;�P/��b��6���&���T���bq�2_��Py�������o�Z.^�_����7F��fbBB�c�YP�0����ts���+0F����znP�bB#~#�EUQ��,���r�f���Q�_G=#�y�C�����CP�E�U�`P����}����� -
�����x�TP��=�����V��s�����`����R��l��q�jYI�xJy���S�C����������3����Y�%R���k�ZV�����������(�,�"�����?� �iv���;u(	��Jr���X�c#{�?��J��;D@�X#�]9��'/��#�a�)EA���^%N��
��x��;J���co��d���Q���
���H5��,���7�.a�Hs��8�@rZP��[ �u�c���5W1T�yU��DQ���bf`6�u�0�S� U�,����@�M��� �\�jI��u Tz����6dm�r��J��0���;~
j���&/<�`�i�B���_�Q5���\|@)�1�����
�x�\�_RjS�Qa��:�^�F� %����n6�F� 6���C�����^�2��x/'F��>�.����G^�V�K�'(�_��rx���h�9�E�6��9V}qSO�*|/s�<W�V ���;��cL2g�9H�Xs
<����KVT_�S4���U����h&�2G���g8*7Y;<��%t�����4-���mNa(b�B��C�xAn���A���������C�^&�p���f>�IAU��@/m���y��`��lx%���`b��`��ah���c��y�ZE,���&@:^]�X���T
��#z�Y���'��1��-QG}@�H�0�{C4Z��fg����cj����H���
Q*a^l���',9����E�>���
(
���Qsb�f�����	��!tb��0zu�,���C(?�- ��Swo�^:���l3���T�@����x�tut�}����-�"�(q�O�a�c�]G=�7������V�t|�d
X����C�
-Q�lz�+C�����%IX@'�����0Po]���\�RZ���d�����X�*�Pgo�";�������V+t��X��)�B���7]���Q�.<y�t����J���R�2�,���~jV����	pW%�/]��~��%s����R��(��R�0b��Kz�V��G�s���q1��!��x�X���g���$��F��aDs���" ��T��|��(
�l������Eqg���;�� �G���x���.$ms��y�W}h!���1�,}U�d���-|
�z���X�VZF^��D�[�p������B{=�y�0aW������Rf��-��sf�����}x�cX���������|��
G0�����J=�T��I�\�3���g�f�q�B4�>��;�����8
��T%J���x;o�f\�����d:�WJ�{����U�U����&0R���-�c"V
���^|��e4�8�XX�$?�b��o3��_��L�w�F+q�X�T�8���������G���w9%9�u�59�������*
�k�`A[�Z����dUm����.�h����;�m
�"�O�G��c{jI����st�i{��<��!��t����f��b�������zD���js��Y������.@n� ��\�r�a7Y��2����<v�u�d��\>��4QV�c:��!+���^x����:�k�[Ia��V�]!�0���,��Z��m�c���3$S�f��P`���3
����N�X�3���U�4��X���S@+�[�g@G_����!_L�6�{7�a����W�G�
{�c�k�!pPW���6 iFq�]��q*��Tk���I[a����:�W���#F�
�kx��-G�f�.1*��������X+������L�ro�"F��,������e���P��+���Sc���r���M�g��r<e�Q1l�n������q'����������v�W���/7��a��&%"�����$E����U��Dm\�Hi=��K�g���+���03�o�U#U�.���1>�5I�_$B6�K_+��jj�<�qn�Ui�����=�P���l��vYuq�n�CV��r���Z:�n����bQ#|T���[h�G�v�V�
��IElWIW��]��J��:Fu�w)H�y���a����|������ad�]����C`�'>�(����u��L�(=���_�/9n��&L�V�pw����B�Ma�^������~����k�Y���� [&kI~�IR�M���k����5Rs�H�i����h/�*�1����y)P��S���a�y����et������j8i����On���oa��#0���d[���_�2h�'6���"�QyO��zL�k
���Bduj�y�����@A�������[�]��a���hGZ��Y��
����(�d_>#Q{����%���@T�gm6��e�������<�G���
����[���,f�tZ��%2���u�K�B�0g��Gg��q��k�CO5�EU�'������d�b��m!=�zTPm�����h�K�����C��"7ui�����BM.`�8G���5R��R��H��������*��M�z�QV���'zPY�ti+7�kY�-:�<�7Xr�|z�w���KP�3K�g�["��CC���-��4���
U�w��[l_o�;8�n`MP�����1:������R
v��u3Z2�]����b;&GiWO�!,���^UaV�h���0���zt������q�\���8{��B�F����
<�=%7�ZW�����2�����)�S�V��r�G<��z ����F�R��������%q�`Y��#t��L��vq	Z���������O��*�3�O�c�������9�8�����QZ*V,`��A����L�L�f�Ug��Q�E�y�}k�G��z����t�����qE��j�=�J�#�8=�~��@a��*8V"�����+#r0o�!�����O���[7���@�)7d��x	�H)���3��e}o1%�Z���e���u���x�m�"��f<b �z�������J�`��14���%��
8
c�DkW�R�,�s��c���G7�����yr \zD+P��#�c�9H�c�;�^f2�H �1��+X_(j������+��g����_���q�=j���s�������4����i�'�Wf&|����\�I/��eZ�����
�lY�X������w��JV7�Z8�
d�Kz��/y�1���]����A���������[g�
:�E�����:�l�������F��a@�I���N�����z�����o�On�Na�-����	F����,�t^GoOF!-n������8��\A$�UZ�=u�����o�^����OJ�X�p���]��V���� :	@�}�������hiC�[U�2�|~���v����6
3���\z}��h~dEGtw������q���"�l@,q7LQ�L�0v���6a�����%_���A3U�x�����?D��pZ���|�_x��Hk�**����(8;��3�XS����&�b��ih
�c��B�g��t8:q����������=f,�^�.�F����!�,ak�Z��J�pr�z�)ER����l���}��K�7]��-�L-��u��bE��4�]�YZ9�N����%�s�;xR�z|T$R������*���L��u����8�K$/l
���Ig����Bj���#����@���Z� a����v@�`�5��fV�8@���AD���_�)=T@�z����^�����um�~���(����P����!�-���
��1������Y���s���N��!����p}�������O�Qmm_N���-��`#�YKg��g'(� ��S�e>�M:�b���*��3/��AE�_RN�Wm�������%Cv:/W,-�.�k�����*nS�6Q���!b��Mu��!J����u�L���5_7*r���|%�5�\�������cL���_�9��.�=�+R�P�F����R���i�t�$�
�g��H�e�����I�}L:�X�#�2x`�>e��\L��:��W`�����i{�-���>��A�K��(w��������;<%\R_L��t���.��)V����%��1���PQ����X�=���f�!Y��
��u��}/���	��f�i�@�9t�k^��p�Pc��:A�-��X�e5�7�s)}qJw�*��^5�a��`�L�=(�'��5fHD�h��G�Xl�l�����lsE��]�����~��"8>�*G5���G&������������:�zl����,\E���H����E����B,8�Y���N�=+�FF��C����Q:���~��GP��v�:�0E�1�L�@�����{D\�:�4oV���Ey�0��W��R��6Z��?I��Z��'M��isb\��}s�;W���'V�W=���N�g~M�F*���m;G@b:y�z�w�[�z=�s*�IW�V�b���`1`k�� 6�W���.
J�'7*\p{���Tgv�����2��W:%��<���s���#B&)��D>��h�.�m8H[=��<>;�0m�L�st}`
2�g������(��
>
�� ���W����*7�^p���9s��^��,c��� P��Z@�b�<?-����d�n��
���`iEK���������c�A@2�K
��� �#x��0�U��+^�#�*`n�~��eNNyu��sC=��-o��]jS�,���+D�J��WH���C7�K��2����hn�(~�������uY��U�0q��Z���=�L�7��R�6V��	�a�����K�j�(��W���/�u�����%j�Z%N)���}����>�9:���M�K*�
��5%�"N��������in�W�������}q�U@��{�J��V���>�� ������1�q�`�(m�{ �����U�����i�t:�t$,��%+7��r�?����D��2XI]`f�C+��o�����e��y*t
o�n�yvE�~�1��8|��CO-��?�	������@1��2���+�lv�$����p�9v:�$O��:�k�����K/Wq�h�O@;B��r=�f&����E���c �'�����k;�����9B`;�������z9"5*�X�"*����2e$��z�LU�Mk�����+/�2����������4���z�	 �Y��1s�EdzW{���@>�= ��b/�j�rB���I��d�-����
��������Z���	 $7;�x��T�����	J	����qVh��7r�F�m���?QW5��XA<9b:� mA����A�~�1p,`D�x�������������q�s����]2�nFI��]���n��w�gw�b#���E�X�E����K�>0���M�w���cmh����uV%����:�h�
�;�.�Mb=1�q7K���1%�K�/	;���)o��/��N|��pG�
,�
��s[�L���L{�u�#��[�����/4��W�b���a�W�
%e�Y�`-��W��zm�����Z�Sl���:����CM=���q3�UL�����x�U���.A|�e�����D��U-N��Fkn�a�o�K������\��{A!���u�vE�8�LK��h��y�g�\r��^s�0r�KW]����ANk�)��6���NK������,� 0J�t>G���
�������kn]+��7��zC�1uD��j&`������"&����N��yA`��o�=��g
@��YR�
K�y`U�3��=J�W�*��^�a�]�"QqZrq�I���!��B�U����.����=L�$��ygAN4�L�P���s��J2*�� �h��W�P4��e
/U�����U�o��������C]��czU�w��N�qSg[����]Xj�hv*�_��T����K����
]������QP}�G��,qt���n�-6�v�Zb�
�����l�u��E�g�Oh�.��/���-+�w�r;�,�6#U������Q�< ��f��OU�s\ue�_rf�Ae��kzn.!@��x�;G#���v5/K�xs0:J���4hS��0;Kq�:NWt[�	Q�_�aE�=�����qEV:�K
i��k�J�����1D�W1�� |9��Jq���Z���k�gO����������(r�5�UT��ij���=�uB���)��C��4B�^6=~����il}�������d��E�`�a�@0:�.d���\���1�����>��m��v��4��_��>�6.����[�� ��4
�M?�B��d;;w������)����P�8�5���a����s��m>�)���-=��;E�i�����$���zp����C���AX-�;�
�)%j���-�������5r��r���J�������/}�������_;�f�6�����T��n�j� N7�<�_�K�m����F��-�j6
��=:�,��i�>nO&���(r+>a"|[����q�^l����k����;�{�S����Z���r�!���nO!?��#z�Y�+x������K�]e9���)a���N�z�84�7o�cX�G��gUR����V?�>�`��j�4q�9�\F������p{\z��B�����^��Pc��s��8b����Is���Y���?���K�n�r6[���&T<����(�r������K*-��(�����0�����Ce_4���e@�M=K����K�=��\IN��N���^�k��u�5W��{��3�Mkp6�M��7f%x�'���B��N7�M��K������qp�vUf'2�������$����;hpm4[�->��
��B�>+��%� ��y hc���zB�����uf����L��6�s��H�j@�$�.w��������j�P.����e�l��@�c�������o��t(���/��p�!�;"�NbZ��K\������\��g
V�T%�d�]aj:�~A	���N����egp�^c��`�v���S����oZ��-z���w�N�_`if@8��ZTG+����X
r����c���7W+/{�7��d��u8^�+g���I�g,�Q#����;�����T5vpl,)H���1��6����9�5-���
�S��#����;��=_-pA=�M<�z�r��:9���;��(�����1������\N?^#X�zE
k�����V"i�2��T�r�2�Q.5���Z�5���B�+������*S�{�Ym!���
��\�E���U����
4�`���TL�65��""���z����p������B�A{�w���~���L]�U]f$(pLU�d=��OJ��\E�T b]5b����Y�t��q�����������[��b�Z���%�G��cm��W��}fi�Q��2��������t��-M��e#N��x�81*�����Q���.�O���?�&	�>Y��v�g��b&����u��s��6dw������kW�0�1�e99���xB��fu�`�-i���V��r����_�O����+�C.���`����/o�]6d����/y�O!������O$J&����78+��w��q1���H�q*�w+�m�R���gI�V\��;���������(�cT�/9�����~1����-��T��A[�zw���e
��
<�X�T�~����m%����������"
$������J�fl8j�� ��M��T@�����c��:�O��/A��%Kc���-���n�8bC�}e���?�����l0o�~,��(��iR���2�mC%Ix/1,r"+�-6���=����\S��u"\���`�EI�Z)ee�����o�Y�~.E�ONc�#�F8�h:���d�a8~���}iP��O����6v�|���������4����b���V'��r�+0�n��>/�_��w��$S1=����r��`��p�Z��X��Z`&hxY���Z����U���W���6��;%��i�5������)g�
����TD���),#unXc��C�_�V�������i[<A�u�j����$t�������~j������1�A\��_2��IbV��/� ���
�*��V�%c�~f���������=��>�
�����r��F������C\#���%#�0,z��J��X~>�boY��yz����w��Y��5X^��|{�P����VE��0��j�	{F���)E�b 5�cW��������$��L�-
�j���<i����O^�S�Ii�h��������*Y�6���TW���\P����^"��5���cHPlLuK��\����F*8�Q�������v|�8B�1��S��]�8u3_����{b:�m*b���0
C[e{g��W���'2���nP\P����#�v��KWWX�.��7
`z�d�s�g�.K3���W������K> �0l������3H�	���t6+�����iZu�)y����@��xE�0.6*��m�($��T�gRw�5��E����!��	x(��=L���&,8��J^�4L�}&+*�v�B�
����8;���V�1lR]�����A����6��xW�F�S,�Eb���
��Di�C�:�g��)�029*4���/�3+����i��d/$�	c��I�����G$qF����b�N��87H}�����x"f�J���!�(O2�(���0J���[���~��������MI��xi2�`.}\W�%v����1�����t���e�Hc����Z�w����0w,N�)�,���}���p'n�>_�8Gy5�P]��Fc�����+�����:��R>�A��3[�0m������I_�[���/�V�3���i����s0�'�Xs�*�X�+3v�:����*ha�C�R�4\d^M!B�w/[����4��Dm]\�D4����h�C�f��R��_2��i����� �Sx����7�~=�W�5[����\����^���}e�W>��ZC�h{��������?�yg�k4~k�V�])����������1]�,(�,fq�*Z*����):������u����b�^��*-j���|;��Qy�~��)OhT���g���?c4�����P!������w�������O�S�����J04umS�c�S���V�=�)��B���/}W��X6q���`�,����O�m=e�j%,�����*@w�	,>���)�'ExA�:vo�����K�*J�DTT��������4�����M�]e~4��)�.�R�������l<��e��]b��F-m����&4�tE��������QS��
���
�y����k����a�RF��b������R9�
k=�������Vk�("`�3��6��b�J���=Nb���+}�\�f��^��V���\�-AV�����"6#��B&�x�W��u>��+�F�o
��D����E��8-T���>�1�:	���l5Qe��ne�2g����K�����9�������7����`��tbIB�KM�fa�Ui*��_Exa��.)�����K�����/j��j}Lq#29G�{�M ������a@YVS~� d�i��_�o��������
�=����������?�������|����Sk{��D(����
��o�3��a~���/�� �����5�E���B����������d�;���c��c{~���q��?R��C��>� �/����wIC�������:��|��Jo�1l�
%W�U�������W��N0��3��A�;��iZ y-uld+I
%��e���U����C�������^0O��������B+[E�.]L��iJQ���D6�@�,8OZ�,�l�t��SL[{�f9�(�5�����{�F�K�\��hew\i������;�me������Nu��3��� S�E�XK���L>�����X��^��~�5xr����hjj�KsD�N��Z�	�N��9�"n����B��WTo!�Dmd���!��:TT.�W;{\��UWG��i���k�o�0)(�E����Y����_3��5%�>�@<CV��l�1��F�/C\�)x�a����Y��%E��E�k_��];�&}��ec6V)1�%����i%�j,�r�#�5�c4Z���Y�s6b�g��B�y�����Le0�W�Z8UF�>�

��
������yd�����_��z]4��X����o�4�6x={t�9+3�%uZR��ezn
!8o�����`� h/W�=zGYX]V��9@����
��zfbr)���{�����I�z}��G00{J��<J4�T,'�%t�rU�������t9"	��o���j���\=CT;y�t� 3���c�5b��ysMzEt;���f�3I��L���_���*������z�����
�,/P�R���c�"��)�WT�@�Ns�h^_����`A,����q:-������H	���]�b���9�@����?{���a$�b�6:\C�61����Z	x`�e���X��_���+V�q�y�SJ�z8@X�&����SRWYz������a��@�W�����_��[�[������o��T���_X�!��DbI1��W�{�^�w�Qh����
��c?0�!�R��7-��=B�3�V��FX�8A�`���C/��x�����R�`�6��d��^��"��]K��w�����8D�	���1P40^��?��4! "14#$35AB6FQa%2b���D��o:�wN��W]NV����n2R)B��2Q����L��@+z�uB7��#S�s����a������F)x�KhxY[v�	��%U��y��P>�:����d_�5�-�������]����t��U�^D_ULcw�V��Q~��t���W����To���^���vc���zyU[%]��V�������^����T;k�&���{"]���xl���x��B�����i��>1X����]����wK�J'��T)����Z6��N���B@�LW�=;�EZ/�^�g�[�Nn
m������s����nS�L��
��lV�1�`�x�vP��4r���������Y�������M�b���_"Kx�����X�R�@�q����5K�c���&��5����C���o�����jq88�M�}�'{R��"\�F<�En���M\�v7����k6����.�	<���H? zKO�U��NE�ir/��s
�4�F&K0~T����|����M,���[M[���^lx��Q��3�l�c��m�����#����d,�����Z~L	-A�
#v=4���w���\	*q����j�������L���;j%�]��zA��g/-"���K��Z!7Oq(�u7m� �D�JvyO�r��d��O�--;tj���+���f���$I{tDX_��{�p��6�$U�Z�3z[B#Z�@-��D�El���W�
=��cjp��W�O��*�\t�������>�D��UM�Z�n�{���������x1b���~��}" ����{��=?l1�U���lF^���!v���� �ha��w��d9�t`Em�2l��nw^Zm��
����DX����J�<���~@�*fQ�J���6��r��'��S��J&�S+V��S������xPIkE��X?�5��_y���	E�#��$1\�b�8����V�5o�*yg"��*�V1�^������EqAK��g��L�sHYM��&)Z,��o����-eD�L"�"JU�����I���K�����D����x�&�(�#��TE����V����V�<�>@�����B���!P����K�0�k	N�QOM8�m8�>��W�hAL��&�����:�I������+�%U�����5��$�CvF	��
�h�	{=o��*&K�-Gp[�d2�|�~@�}�nAm'�W���k��L�-{�W����W����w�>������+��6���p� �t�rI^�~X zIcz)Q��aB���4�����eypPK�jQ��*8��7,��x�;�%���cm�����h�,sCm6!#JHn�!zz�����F�U���9OjO�I�g��t��W�0R�
�
�K�X}Z.�s�CX�����
[,�R�*�
'�Z���k��A����]��mph�b��T�u&?~+��$��Ey���{���Q�C��M���}=�7$L������z/H? z<��w��U����"��+_�c)B���6����-*R%"�����RC{U�	Sj�k��q��J�f���G�+/������������*"�H���J��5��4��)��b�������g�$��P���(��\�UU�>D{
�_$�=.�+[H=U�\D����[Ur��fb"a���D��T������eK���tE'�J�E[�=�����j�!�����
� ���&0�Wm\i�l���Pq;nO�KcP�0����qu���\i���q�w�wl��	)�����;B���:7��\.��I�r��
������0��.sV����.l\�*V������������f-��`]�;l����oW7�!�A�YN�Rc���b�i
�G!uM��R���V�nH�x��s����jSS�/�����q�&?k�]���V����V���rl����qn�
`�J��
K]�������D*�?����<������i��.��>NR���pZt�\���2E�.P�V	q�rT�%b�nu�}E3���e�����5�M�����������/iq�wX2�z��������x�N+�\�G�77	�O����N����NDn=�x�}���g�o��K���niR��
~ |�Z��&��.��L�ne�Dk�[F%�C�����QI��F3J������yN��������n��y��:��M����'6�r�[��q���wiJ���RM�r(1l����O����$cQ�s����2cm<��C��;Q��7�>:������A�X���������Nv�U���j���*�_��s��(��5\q�l�l
O�EXRI��i�m���%�����Gv�O�Y3�u�]
D�M ����'��P���d%P�&��+SSZ��{~�Ge^�0���C�*�E��&Xt"��#�/�'M
L�c����^�����Y�T|^b�������	��eGGe�Rd\�q�_D���iQV���Z����t�t�E]5���v���9�.�����=�[W'\8�n�u�+U��Z5St����y�D�����s�	h�2q@����B�jH�mZ����`k�$�u�����%�^������!'��T���*Lr���G$AJz�f��_����� tV����>	!1AQ"a�02Rq��� Bb��#@���r�����`c���	?(��D�2���|
ic��@�E��7�?���������8<	�H��F��Vd�`_V��������	��	�h�[w��@�]"z��\�A8����B�#����e0�����3]�k]���qL&�{�FS�a\&�fr��TQEy1�~�ZZ.����a3�	�b��w@]�E���W`}NM����iF}aU��&5�j���.��O0����� ���{]�d�K����O8#��@/l/x8x���v����ZA�v������@�����M	$
2��y���������e���k�����`L~�I���o�������Z`�(��)�|��sNs��O2����;����O5�4)�$Q�H�E��O�i��F7@�!Z}��'>5�q�$��D	�u\$@�z����0�6���e��k��u�Nl�M�*)w#"��G��n�)��h�_�?�fG�G���Z����z�;�]�Lq�w�!��v�p2���>7��mL�^t�8��W��m|)�>=`�MBE�r��.�Z��z(��x���X���Q��.@����X��h�P��s1w�z��Wm��\���@�c���P_��������D=�����k�:`�De�3�ui�.����K0�`U������Y�8�V"{��aQX�;��U�����
7f���
�]���+ Ch7+0��Y��U��s�*�x�&��d�D��&�Uf	�����>�<�r���:C��'�:a�
�����`����������-B��1�F�Ag���UNO{�9{u'*�M���t�|(Xt9r
�X�y0Xj�7z�FU�?5hK@};�V��a�����7����l���2=U���Oy��B,�0G}�V��z$a4�%N�A8�~x+K��1�'�/mD��d�SvLz'�x$�s}eZ}��\������n���Y���x�8����5ih� ^���p�a|�����1��&p�4]�>
V��K������s3�oV��������I
�Ah&<����5�J��c�a���_�}J�,Z����X�2������E@19��*2((�.������c�Q�f���%G7y������|�F*��:)���R�������P�L�h��t�t��D��8�����|��(����S���.�O�D���39F�j��~��38���Mm����o��Mm������gh��7jfk��Lm�0#���1��������k��L�I	����5��`9��$�
���@��X����f�]5���p����"',�VL!������}��NI��(~)��\&`�8�rca�MI�8&m��a�wJ��G����T"����v��5v�x9|�ZA���^�<b�]�6f���G�`sDK]�a6�SM�#��N��x�i(K�2;�����R>k����4z�!�~O+�i�
e�v�� (L�v������3����.��{:+�`0�CZw�'_������;����qde��u��tLT|�.>��8�79�6�:�0��8r��.����W*�UX�z`�	��5�*z�����%� ����7�c�!Y:��#7L`���d�}n��w������VN�>b	4]1L{��o�g�U���H�#gEe}��v��/�<B�$��QEB�(��Q"�(����,!1 "23ACQB#a0bq����&���j����A��aS&S\�qr��\�)�����a��v��� �dJ���.����C.�X�cW-����1��$���;��S<�f�YS�S�"Jn�?�i��&��s0�
�P�IM2���}gFT�Ks��Bi���~e5�
��;AE~D��v)�q�M����9��j��#�T�t.&�,)� ��1*���.��U�]*�Q9S8w��QT���^�s0��
�4�=eB��
� �.c�K~�������SH�O�M�������$�)��#����g�=e[,S&�:u�}gN��? �[�T��f�8�	C0�ng��D��Q�)���}	������*�d
���x�Y���i��������_��]9U*����^��[.�}�\���^��];���w9�N'�t�����
���7��0������-��:x�QR�f�h����Y�c�k*^�T���:������8V����B{M��nl�~aD�O��:��I����*Tu9��
F�L)��z�W�:p�AL������.t*H�3�(�`+��#�t�-!���d��=��RZ�6m_�*� ��U0�^%5�=EB���Ye3D��/�*�H8\�D��-�mC�k�%�[0h����x�QR�z��u�H�R�C�l�# �C)����4����a~�`�.�mQ���`�N�O��T�e5�
�`��J����k��
�L�'4_Gwe�� �#�t����0��N@�t:�S�F�sqQ���U����gJ���#=����C�l��=r�k:�ic�;�:�[�1�f:%W��)���p'���gsRv!�p�L'���
��'7*gjM�j�4H�.d����8a4���ppnkM�Wn��L�1W��0��8��k�n���nfA�'6�
�?�z+���k�E]k�hpp�B-�sC��&�\�r�k^���y"!����%�['#�X^a��S����-%]����:�uF���3���=�2���P�"��Q8���	�s��*��c�l��f|��f#����L���K���6"�sQ��r	"N�l�tA#�+����S9��@��sA�6-!4��i���=p�m�'��@}�Z�{O.��`8,���h��������C@~
���;E�{���?�64��U�F�?�:~���!N�	������!1��{��n�6~?��{�lP�+h)���'� �\E��]Y���
�p���������HsL��PL"$`Ism��������SA@8i����c�L*WA�x�Z��T�F���<.x\����Q�D�x_��<&WO�pM��N�$�+�!s���V�,*����! 01@Pa!AQp��	?""""���n����.�����^�{Y��8fg���<=��l��������?]�7O�w8��O�������8�S��t8fgl�q}DDDF���#A2h�#����(!1AQaq�� ���0������?�N(%���NhN�H
����%C�|��_]��e <���zx$��I#�t\m���k�� ��}��;������C ���%F��%�)�a5�������E����&v|I��uC��	^�8�Q�;�'
�
��U�<���2��#$��R?xt�/$]N8Q<�Um>���qv��l��f>��AYhS�^i�{N��t"�[��9�6K�8��X����fv2b"��-�R�
�KD�Q��&l]�I$\�&B���P����z�0EV�	� ��=' :^�eJ���v��,�A:?���Q��f)��	��	��B�����;��b�����pf*��������H�
Lc[�i!,P$KD����H�b�@�J�`��//���v������g��Bh339S�1��#z��b���8JG�c�B��g�a>��0,��Y& ��}��������=��p������I�
���xB$��c0����M���(G �4�C��4�GN$b,���l�c5����}�c~�ts<�~�������IH�C��"�MAg%��&�(���/�#�:�]��`���qr@I��@ ���`�
4����� ?��P�fL8��z�<�9g�X�gd ��<���B����|��`�t� �"�0�h
U���D�|H,��n��3��-�h�`�\%���P�\l~?d�	���~���JID��1m$�����A�$����vH���@A�9N�	@%C�d�:�Ta�Oc�����Ao�F��"��(�7 ���V8@(wn�"E�|w��s�1,B5S��#&��������e���/����
I����$���2:D'�9�?�-���d~41�l������J����9���S�f�����X��@����HHS��F@���)���B
���uz9��,S,)��
`@�^�%���]0�I����#�e
�X�
��� �|�9���*�E�#��h�%�8��3�0IxP��s�B���zP�p$�o9�����e��$�2��$9<O, C1��9k ��wC5����| �k���P���������y�TA!I���P�2~�����!&Z�Dv�B��\��]�I$�<��r@���	���)1Srd�FSy!��Afjc�b�k�$B5�'��HCL���w��]Q�r��d��-�����c�1,x�@
sl���Jo�c��&cDLb�PXG�����3���;XB�Lj��0����
�q8��r�J.A�^/�B�l��g�]��<�������x!Ig��?�^V��o�oq�����H��;UC���qiI���90��'hfd�^�����@�(��F�K��u�53�M0���?������<M�~��B��b�-P�:���F�b��1�(70Q<s�)��Q�P�BYH��Tcf�{�+���@���3�*n����[� *d��'�;P���\����}�;�g�4�r�L�q�W��,���.[���4A�"�����C������I�mz��P�bPfnB�N'~L��2IDm,@I�(�g;��F�BYt�����s��5z_���|��W�o���BK-�KC����~�;8K[�BQ*��K�P�{������
:Y
�YC�NJ4^��wN]f��JA��%T��Y>���	�6���,�Ia�"�����r~���)d(P	T��nd�c�	"0IC}�-,�3��D�x�N����=#���$p���q��3�����������*z���XY����k=�o�$2"�G�`<[.X���X6��p"�l���8��
L�G�j��A0�h'x���n���a3DW��D�	��F	@BP0��+�8�f���"��=23`*��}N�dejw�0�l��fptY	$I��L�g��d#4`s�pNb����!`/������g�~0���H��6q'yJJ��HO��;���g�xZy���������@�c��'(>\�J�#�QyX�%,A�����q7�'��1!O��{��$�!C"��a�PxD~�r(��.!�&�g�$�>���1����6�����)�I
3�H��LH��(D��4��`��/��%S�'g�6'�O��#	���\��������]�|�Xv AI3y��R�+<�����b
�|�E�of

�'ymb�4Q1yi'�(%U�	U�j���qT_GC�jc�$B�909�He���od�%�V'�����8��I
C�5j��� ��'1�VX�"b��Q��'�Ji��4!�?{��V���x�"�6�~�0��4��$�V��_�;���#������0�s������	U��^��8H���0�1�U�j�abG��IjX-UZ���'}Da��B�V���	��������'sh�#P�z��7��	��I��� <����?��� ��"===�2*����|2:)��E�<9�{dF@fY�,>��`x�����"P�A�p��������_x��L��O��]�@��&���JX��~�gi�b&8�7�!	W�$�,�@lN$i��'^�h�^��u�������)09
���QH�c�0�x��"*bl�braA��I�%A4fdo����M���z2�b�+W��^�5�&�P�jy��Q�T�w'��0��L[8���=���,�eVC��T�b �����Iv�����-�2����V���l�m�������%Z��h�S(�D�|$�k�0��1!'y��8�� �>��$"uXy���i�r!P�(M��+%��b���N'b�����&�2Ehl���p�J���I��)��s&���2A1�e�EQ���4k��n�8�����D��x����2�.��X�=d�Be"(-��AD��I�
)"8�Qv�O��q;p{��a7��)�[G��(F

�(���|G���`�2�gC�i���q1{�!G���+��+zQ�T@�"�K������vU��{��BL=&��*�U��[�k���o�?�T8m~����6�6f�dP�X�;#�yM1q��I+3:������&Jj�>[�0��fIK�R�@
�"P��-�b@�j)� �2���Fx��1�����Jk�Q���7�t]���
�"��	<�B�X������4�T���fQ&�!-�x�&$�����Vl	��	@�0�L$U��]o;���P��~������2,����8�~��8�or����(4�Q<�	����p# E�F�t��>i�06��
q���rFT��}��dc�%I��
t�� �42��;�����73�w	^ ,"�jE����Akk��iaJ(W[���N��N�3Yu�����(�P����,bX��Ncy
���bQb�E�����,�A!1q����"Aic����%\8jQ�x-�y�#*�R�.4��;�fB^�r}u2�_n(����s��(�oV
;�"�|�������T�\apU0���DE5�)`��������#��,��^3��MBz���N�5.H=���f�N�� FM�d�g�u������1�)����DY	����G�&�uabP�7�5���J��9�Dxqn���O���}>�8�`�a��(��d��K�1���1P'�a��I��*W���UE��t&b���@�H(lzF}����$G���3��A+	r��hH�@8�v��"�.#�t&!��1�A�Z���Ok?b�*�BPQH��&�1BM�z����*��k����G ��cW�d`#��B2�M��p����M@7F�����`Ds{�Xv�2�s���wO���@w����@�8����A���%N��4�\����=Tb���+`�	������)�Mg@�6$���~�` ����2@4\�9�,i�P�R���H���BAnL�s�*5�����K��8��w1��J������F���&RY�<F��<3�z�"1�
}	e��%��?�y�FI�fH��6FX���~��i��
~������C�Q>Q�9�9T%�]s��nq�KK�w��<HtGu��")DM���B�����2D�T�3���+Q��(
�F`z4&O��P�0
$g5&S��SUx��'�����1���"CkW�#�k5=~C�$��b{v'?8:��'J��D �����f '�v@NRRj1�0I4��m4h�v.@�L����oa0�	��8��t�"3�&Ml��Xb8Nx����LF��?f~D�/��yd�M[Z�@���S�a/;��4�U�c�5S����:rU���OR����?�~��!�L�u!CE�Lk�E��Xq`9�#�V������\�DZD�`��
��4��v�h���P�$��������1��A�����&�'�l �������F��g9PRSz�9�y-�=��ayHh��K����3p��@��?��H�g	D��!2ph�P���@�I�����`A��x��]�D&�_`�w�r��s��i�7s��(%��y!�3�>�� 9�0>����8!2��4'F���h	AEB��X�+\z��&�NT�c�2���9w�?"d�o6�<O����@F�g�}|�&G�i-jy�b\��������X��mi��
h��#��(��a8F9�qh%n�lGa�j���)	�p�������0����=n0#�I"�pZ��y,sBu��B�j����@��6(�R#��g(�'�C�>"FG���	QE��0��_�=��"���<�[�*��Gf��dSS5�s�LRrX�5[T<7X��hcb�)���%Tl���L�'�9Qg�BH������`�������LQ�Y��$��{(Q#���`�t)�|{��2�@:)��������wN�Rx�@)/�vhA���t'�*mC��R�
���@Qi9O���%��q����vi�HlX��x�9�!B��L��@oT�A(
`}X|@
�Q���B��c���e�����j�t$��y���.
=t%q����?� #���z���1P��X�����a����V#dPZ���E��gN;Ra�b�`h�5��1��%E]�Ezbt��fx!�9�����`@W�b�b���@��������$Kd?��b4�
N�1-����*��e_� ���������W�	d"��(���N{l�8��
�L�M�(��(Bh��]�@��58:PxD'�H�F(��1
p]�m��e'%�S �!&i�7`4~����<@��a`�P1��b1S��7V�p���9lN
/�r.����������N�h���DA{��A�[P���6���y���er#1"��$
%P�U�X�/5S�.p
��X��3�Tz������R$��
�&K�=�����8.7���`L��,	�t���>JBo�����D}gr����y`z�E��4	@��������~����>TI���?�h5y�)�|x���F,H���,,��k�c3 m�sB�qe�9~Dj\�2�8�����:�\Q�&�0�N_@2�qc���pf:c
��E��IK?`���U�qz�����!����#|��(��/k���M������S7��3���a��	9�%@
�#!$�#�Y��H N�& 
��plXb���@��|����Rx��]+<�F�nk4'����;\V!����F_���5,�q��B�������9�K	R��hFYc,MB����@e�f�����'�p!p�hA���}?Q����z%<�3Bq]X��46Y `���9��l��m�$M����5�j
�]U����q���D�b��u���(J��T��? H
�`�\��o:�P�X�bQ^Ndic����X�����3qv�����#`c��R(#W�FX��2���w���}��3�����	h�J� 
\N!�h��,&��F��
PP��� M��6X3���#�Jb���>�H���L�J��b�%75/����J�h�2�j6�X�� %��}�D�ji���
Qz�����f��R2�;f��(��~��(�B�� �����C}2gP�9^Zf�q&��e����@0h	rY,�� ;�-�A9�%�����@�����x�FK�;���)�@��)������'�c<	��#
�XC���e*���u	&ks�JI���A	$��E��F,%/O�L��t3�|���)���}���?�EA������})b0[4�q�*
�J`��A�h�he��@�-�_�3��h����P� Q���0)��W6&NAQ|�:�4C���b����I���E*Fz��9�N�RV�rD1N�
�i�B5&?`_�B;�"�=J�	��luC6'Vbh�$��FO<2����B��E1��H�9$95SX�N����!&������tc@"�%��)cbPj���22��a9�3B(����k'�5��)���:vJ��`����	�i���v�e��&�����A	]#�(e�4a^���	��;z���{z��bj��(�G:8�o`�y��U�#%f��[a��"	���c�b���	/G��P$,A�$cbY�`���a4��@j�Nb�z��K�;�����hc L�:���y�)��SL�LErw�c�!�#@�?�{s�6#?�8�������1
�T��/������"y�VP���BVy`�������bYx���H@w(��5;=���,�����F���S��D�����d�o]K��	���`,	
�+�:��4��BViQ��:�8�`'PRvx�'���� �l�%��1��5\��>�)Y�=OG���$8�l�c$�Bs*�=B�����&����6)B�f=�� pH�I8��mL��� L0N��Q&�e�]z=�~@A&9�-����8�l$?����O��X<rVcu�@�	-���n����!)MmB;a�A��pD��f�jZg�h�N8ab��6K�Q��@BA�����	�j�=�$��o���$���o���I�H�@�
O�4lZ!9���'�`MRL�Cv#=)�������������l���}��f��,�w9hF0A.���#"�L�@��~�W��H��	����H`�#�?��:�5�"{��bb��\�"R��#2V�Bt,|4������b!.�j:��&8�  �W�E����(�;?�|�����������L@�"�{,��X�Jb\�"�<!H|�5,�*(��I$����d��b�N�}	��+��(o6��
�+�dP�A��C�LB5�%��1ewU������������nDSos���cz�&�!��+,5w�Xr����'' a�O�,yDZ2��r
[Z4���Oz|c���v�����3������t4?d9�=��3:�y�(A����Hhp{�������4���=�Y�����.�U��6%��(�0>�>��!�p�����
7�#����2�Q�@���c@X�U�^��3\(�����M��$Wp#)`o�����>��=�����NC�HBB��QgU������	�{��y`�D�(B��QI�#�K���LT&A�JJ���K�1a�Xd��%�q��Y!
�8PXU�n��F&�����B:�>U�B`P��Y�	#�\�����������<.UnY�8n3�x�Kj��!�B�b�FVA�A+��#�^\tk
|�Z�����^���������`T��|@����?�~�;��2E%�g1fl�`�w1p����
�Dq'��n���Ds�,H
N���8�l����(#��)y�'F%�8)�q�)Af,����
��6�Z8��7�%+��H�t!���cBO�K�%�vW*k|�@�B��7j/����#��	���b��k�E4[-�����P$OP���NDABN��)�':�V�E�=i#z|��"P�P�?��`G�{���0s�$&z���w���42
^Z���Fa�	i�jVK���pWkF���} �5NF���	
n��������?��C!��_��0F
Q��(�a�"7�^!4b@�M��"9a�(Jt
�����w���Q}F�������:������0� E�2����F7�����;FX�I�0Dp:�/��WDV ��-�H*����'J��S3dc/�Xgpf�(�l�x�8��3�00�*��OI��'$�<�3��X����X��#�2�cJ���Y�5�c��

)����R9`"%7g�_�3�@���$��.H��6��,�J=������~.�1,��K�[39�0�r���R�u��v�J��3`@�7�<�D��H����3������?�@���.����:��j%$���2��t�X0��'h�r��
��w��2��b<��m0fy��!����nU��
Dt�v#�=O��$%����<�������u%o�*��J�!���W{a�4�-�p%�����Pk$�����IG/(���,�# ���Y,�!�!F�8h�B��]p�Y�z�1RQ��23���!����;��
;��0����>
d�����2|��vQm���A�4P������h�Q��,�!bK�e�B8�~!��J��~AG�|�o4^&{�9�`l��:T�?���J)����Y<g9K:S���VX�Zof:�-����!�X5\t����M���0������r�"�E��FW�q�fYr4�&I��������P!�.v�3�	��a���< ZX�g/`������'][(q@1��f��Q�	��q~�&�@#�y�Dbj�L����	%��"A$�������g"���0Ef���@|L�C�I�&
��p�$>�����rq%�-�g�:��S���e�c������PX��XL����9P3deiwRE�A#��r��d��C-�A3d��2h{3<�� ��<�0L�1"y�+$�|8@(|�� W�p1���M%�� b,L��ACN�e���k�r%�l7bvc����-�=gV���h36�eH@<��"ZZ��\�� NlDM������T��'S����"Z������'Iu T�p��IQ�lng��M,LX!
}�y�`3�	ic&p#��Yb���\��$N9�H[���3��G�@o��I�ZO���,�=��8����Cy!
R}e	�]�bd=�B�H9�5{�W`���Z9�GD�����',��Z�3X�	4�����Y?X��%A�x����x=�RF�E�����DiNt "�S��'��yZ2���H%~�	�'����`�DR@`�MZ$�oL���aB*����"�+5q�l	d���4'�
�j�"	�@���� LW�
"s<JIJ�a���G�'��	|���A(�E��<Y`q��++� ��{i(�h?I�?�P�?�2��� �s��a��5'�	�����=������i/�9F�l����	$�qpgBZ�s��}@u���e�i8�bUw��@��hL
�op&Q�@���J�Z��")�7�l�?bj	s�����`&�<�2��I
|g�q�^���������^�����H�a���X$Y���� KDx��Pa3�B�h�"����	DN��B��ln�0l&+Sr?NP7�J0p	��?laR&[Z�1���/��@������?Y 	������bxR�e7�+5�UW��`�&Og��BB��, ��=	��5/
����r\�=�k�`
���l�	���zi��j��R&Ic��8!G><v,��R���

;���'CN�V�CPpR���I]�hp��\s�������p�h�������&B�����5`i=L� u4D�e0��)u9v���d�n@�m�$)	7�@	���5���L$p���0q�b�n'������:�?�]�)F���>�� *���������O��`!���Yl������6x�^t T��Q��(T����< I��X�K~^�� gb*�[y�a�B)�&d^k��K��+9H��	�L��@��3�"���"��Ls��P�ZX��I T�x����M�c�y�`��*O�!��8�4&��3��#�`��1�",���<���Z\��&:cx�#P������J��R��E2BQM�B�^^^/��,��X�-���Q'L��'��D�Hk�-��(
|?��@��i&�g�(&bg�E������8U����V�`h�
Z����2?�g�_��Tn)<�0C�|`�kp0��ZtV��.�d��t�%L��0��O�$�����<�H�
�i&�(`�8�p�Mg��,�Z�
�p�\���ay_Y�J�Q�Fy\�3�4��k,	�tF���WK���)�u�`b��������Y�8AX���8Sd�����-6)p�%(�v.7P��l8�MEB���@\\�M|,C��"�<96�D�O
bZX��F��(8@T><m�SE��'
�%��x�CZ_a_Rr}<��g����y����B��y?�A�|��?��B�,c�b�����Y�I#���$K"�3����9'l��jFM-�7�SO�4���D�;�h��e88F��#�l ���
Z�x��,	�S��7P��� 	�x3`P�x3����|9�KH��Es1H��4^��J���������39a�z9\2D�f�����G����O�;��S�	������	�%�z3BhN6�������7��f���"�Y�1�B�$s\p���e8�
o��d���\O�u��I�=�7�H�X���z�6�g������%7�!%�`��r�sy��5�(�Llp�c� T��$���v�ZW��t��i�aS��Mv���-^w*����K��8X����Z�x����y������f
� �>1�
�E�������Ph(��?qL�s�4U�����`rI��@I;�4$Yx�����
:�4%"E"��6&y�y��iZ��<�Q�Dpxp$��k���5U�3��?�2B����.w�}��c�*��N[�AWS�����a3�uAy��/��w�=.������Q,zV�_9^�bX����9�L�$�@B~D���X��r4��=^26P����!i�������%����c����F��:8;V�����f
?�^���y�Fv�����P�����@QS�B��*j��h�U�u������
c�y�1�o���ih�41�����>���v�X������/K�46�\b�U96*�zA�v�����h���o��AT�"#nv����K���)X�0J",A�����0eHYt&J�|VT����W���t)!GLL4
��

4o!���C�)�%��`]��	���%��*�yw1X,�B���!�;��AL
@���DnGxzE��x�9�#��!a���q��J��tV�����z��L��'�v��E�B��S� ��	mI�EQ7����e"*$%.}�@DU�a���gL�f�8:�D�	�,7l�@15�����A��-p�����\j����e7�$@&����}�A��QN�1-@���
������\u�%=R����d��h�0Jhy��������%��I�K������>����L�����:%�5�l�!���5u	?l���*��v�fq	G���� Dc�8��� 
�A8H��G	�"�Oo��&��{
�C@}��_�xK�T�s�KO��1OM)($���!���  ����e��	g��^}��2$*<���&�E��:'JR����������8
�/<��y9Y���VJH���"K�K���7J�*<)���jR`(wc��)/��L�E�����%�J�7<��<������]�[���	��YdA�����+�a���P3%<�#�`6�bY��"��-�R�J9I2 �DkF�&�<��t��bd�p�R��g�Yhd���� JH���c�9��`Rl�?����G�1J-��]Z��$�9�&]��@4��@��B�[����-%�M�_��9��`" ���kay���I1;�=�T^}�d������P+:�\�T�j'(���G��.�D�����:��z�(
�e+P�I������l�d<�6?�e^F�%>?����j�$*`��B6~<i�M&��F1��32���A";�2mn�8�CT@�fS1����s�zJ-��n�$�RF�[1JN�DO
�Aq�)J�N�4��d+r��v�-~��m��������<�4��x�!B��P����';N)~2h7f�Y���P�?�{���������P4M�������9�
t��Q��]�l�d����D��19�b�

F�D��_q���*<%F�6|N" �">&xaEQ:��`KQ�Ma)�(�'w�e�',�i��4w���}c��<�-�	�H�Jy�N^���!(r����(�f)bt�
�GS
*ho}8�PQ6�6r	D�	c�a��"�@y��=���0����������ZL>���/9�0��4x����pZ~��C��+� D���S�1vI�*����-Oh�@~�|���#���%��?"Y����"3������:��3@�>@��X�DL!XjKpQ�y�G�����8��&�;��(���H,�����	���T���H�P6C0�g�0�i�a�F�`�Y�k21YX @���r J�I#<0 ��`d<�b�G�^]��V�t���|_.E
��tk!h�$F��C3
��(����)C�/�����k|b4����l�	%�H�#H�"��IH�%���=a�I7�s�7�%��2�I"$��B��Uq!GZ��!��J\��S�%B26��CW��W��@Z�(���XG�g�V�"!U��PH������R������d[d4K����"
�*$2���K6�b�����0~���[��P�~l�������	�X�2���B8�ZDzn�4�I���E��H�u�9C�S.WxwZ8����P(��>'PB
 ��pm2��	C�kNjsb ��GN'W��H���b���cF�T���L���s��p@���0$��$#1Y[�r�����l�F������G�" :��@�r�����H�a��:a`+�L�	��5M�H�����nz��J-�:������o1�l�t_0a+6�:|��c�f�b����'
���D�I��Y�a�H�f�X��1ND�/�c����tMS9!�.�H�,H
�>)�����mB��Lr/	&K��E �D8}?�
�6�ZLT�R]�<�$�78k�J���1�`E�v�X8m���!G(a������c/���0�A��4����_��)3�D�$Q���q�8�xvVM/�SdA�=e_
����IQ�z�^S�R+ ��4�rLe,�d��(�*�j� HM�m��.�x 2m�L�f�`
Cg��DB`�t48|��$;Aryb�X�0�[I	���@�}��:��PQ��Hs�,�`	��o��''T�D���y�o�
�1D!$�%����0��2i�D'HR�����&3@��p�!&�R��2u�$�>C�P�w�b�S~�/���@ax}m�p�l��*���j��`�2�MK�MUDW ����b-�!�\�I8�Dv9h����af���D?H`�C�1������"#�.8> ��Bf�Z���	��������t�X�0an�NE�c
��Hpz���g�yD�u����r^Vma|/��x�u�<�]E���#�FU����
�E�`�EX��nw�7
��l,��^���B8�X��� �l�A�5�bf#�%�}�������d��L��8j�*A����$���	� T#�R�
��=�F��T*�A����.��;P��I���B��Rb�u�5����2`_�>RwQ2�o�OYV������5n"
�8������N���S4R����LX!
xb`�C<*��0�V.���z1Q@����b�����7� q��	�8"��?����B����
|I�L�*Y�	��������7�*,xh`Q�4�������o�
L�R�oe�th��������/�L��y�@��Y9@��(�Qj!J�=RD)��.�a@���\���@���)������JK	%K$�`�T��ck =u}\EL�����$;1<�w ��J�%Y\�C,�������v/�V@"�Q5C4��2j9������&l�F
L�P�/{d���=��&�h��w��b:�hD�0���Z9W��y��f6��H���)� W���(������I�p�����~�H)����	�,�+81~=,���N�$�,2Dh4���bZ�@�	X�?����V~Da�H�S�q"`c�	�z�Z���Ty��� ��(I0��&�F�3�|������U�������&$�A�I�%��'���P\h'V�D��%�$$ ��X E)�*�$�~j��<,tkL�Y��D@>�ke=JH��A
q��c�<�6���E�$�!&�� 6��r:�(	��
��"M?����u '���Sp��l	Ruf �������r	���C�1�x���L��PC,���"������o��pz������)^b����h�s���j�#o��qMJ)��7Yd�q�}�>����
�/F$�y�=,����*�{ �Y�`��B��*���pD�U��B@�6)���J u�*�D�aC	6Be0c��q`TC�S�'��\OR���A�(��2�h�*��DB-�&\���%@�iz����������V���(��&����	�T�a���y{C�����]a1c@L�X�$M,�	��������#��B&:M��%��Y3@�R�RS�����$C�)�����J�,\��"�9#xWAb��sy�OS�&v}�l�f��5��hLAD3���t(�gE�e����Y8�qp��k�������T@�pcRS�����8�Aw�����C\b�!4�&K�UH�]9���3����uF��.������Z�����H��g�� �9
M,���`�jH��)���I�����b@2A	{+�B!$�	��@c� lr��m�B{�G����Z�k��Aj�c:$(M4��uF���,��>�E�v#	����9.zvV������y�F!��jD]#����R�0����n<b!�
 ��}�;X{��C#������&�N��	��%=)�Ez�9��"�S������I��<��2�7����8��V����^g�DJe�f��<�}3GF\�8
Z�Y2:]���"����,�ke��
�"x�+y@Qs���|�N�\g"x�Ki���zYbH��Y�M��
��w��\<��|dR�����r+��BKXr��\@LA2�����������e����-k�)�1�nR��q�[2V�����P��1D�a_"��W���S�G1o�<B#AH�C'��N�-��6+0�n�d�y�%Ing�Z�.	"��z�����Z6eV�I�pG�����P����P��	��XG��E���2��Y��j�31�	�t_��:��rOl����F�^	��C���p`�f]Q`���$���%WWFS4���T���*��)��E��I�������6	�2���������ZKPb��2IL�ZA�qa�����QKNG�o��I</ �Nb|���A,���8�h����1	�>�-\c��fi "��*�`cJ����2�	���	8���[|������'!1AQaq����� ������?���7�+��������my�W��"��)�r��by��X�U�V��aI���%g�.����T�1��������f&g"��:��������]�JA!��]�������^e�:�4�j��b���y��q-6�Uw������-.0
*���\1T������:����6���m�]��i{jw�,��X?�[���c���>XX��&Q^�C���u?���7�~3���������{�&�����5u-��������%sA���{����q�����5������k�PS�~��V!;	t)L�Y����n_���L���&�(e0�X����D� �j��
�u�l0�0��@����H������]�`�O|��=��E����
�Nn��D!���6=��5�����!a�dt�7&�4�=\��M���O�*���s9��L�P��A��E1�O���.�-V�f�EN�
�_�Xt��]_+��%�E����.��� v%�������O��O4����#��+��#�����A)�����M�2W<���C��l��x�1�Atv��{��aI�^��R�`�q����Y�?�F��U�������%(-V>�:|�KJQ���g
��=J���.��7�����G��W7#�rp����d��Y��0�����_��)5K�
)5���H��J��:�4r�;���K+����'%�
��6b�����.���k2=Y��n����������?H��[�G�g�'�g��Q�XJ%YI�o�{��W�x�@U��"�g�X*���;��x�J�Dd3emTV��-R�cI�(�3Q^�=�w2�m��w�.*��k:���n2<S~�K1��>���4���Yf���{nW��lt^�n��MW�W�J�!�YnM�X��f,�������EY���4���:2���o�	O
���w��}�'tB�@�]�z��
���HQ���t(��Y��3�_o�Ue���G�	Z��Z%�����1,�R�j��73�%_���Tf\z~O��/�U�E
��[����c`�X�����:V�����P�lo�f�z��Dn��~i|����4`0@��#Z������c��J���Z����V�
u�H�3Z7����c��R�7Fb$4�{��uH�5����mko�o�D��n��^�o2�@LL���{���Z��=�Q�L]_L�g�SCF�\Eo����_��]�+Du���\�k�D�9�Xh0�8�o �c��!��w�4���%1����r��w�-4�=`I]��2��|\GHa�28��F�Wx�q�S=O�q��he���O1����.-�������/�T��g��{MI��4QdI�@�2�Gk.�@��fU.����(:^H�&��b��;����o��?��gF�����]c�+��e�']�\a�sZW�3`����7������2��������
����j�CA�s	j1�
\Wf`ja��~�)m��C�����g�+}O��e"n��6 ���:����f��
��h5�e$7o$�Z������`�$�-��s�j��6�}>���5����"��O��C���h��~e2���"�q�.�U'�A��"�F��7L���w1�{��
�YJu��eG�-��}����L�4T#�1�����RrP+��w���R�8������f��_XW��,���a�����*`1��
wc�>��>��}>d�7�� �")	��F6����${�e6�K��}�OLZ�y�Jv���4�o%�����p
aXz;������h71��Y�"�Y��gX�h��&��Q/�NQgl�8pC����IQVq����j�`5�9�a�z�J���X=��e�ng��F����%v�lo�c���0U!�[�nC�K K
���>k#5����t�MB���]h#���3E)<�>����SV�'���
���c�F�
�*�0�B�`���!���`�EB��8�B�P�l���S,����	�+�;o�.}���]���W�^}��:��'J�G����jZ9?SK�����PB�8��f6�{=�,Ee,K��������
�O>"wn�N1&{J��~�o��4�7�W����~;@�W��Y������a`t5g�2��T!���Sf�.��T�.P.7��M��y�2���5|��B����=���,m�2���k�o�,��4QJ�[m�����Q����nr�	Y6&lAT8]up��J0�V�4-f�c
�V�:�g�[�
:HZ4�U^�X�\J��&�P��:B�������*6�
?�Mu�2�`�����1u0d��8��Y
�.����ic��(
#C�^(��	A�����q|"�	�,�<�r[�*��SG�m��������H�HU
[�S���d;��_��3�?f[�]Zpvjf����p/W�����\(��r���,K( �Twwx���Q�l�y��[��&F��=�:a���9gKR���W�|�:��)�u�:�]el
������%��u��*�b�����j).A 9��j ������M��������r��qV��c��gG���U�w��x�������^R�9����o���
9���w�����
�V82���r�CD�5k�K����
���1j�_v���B
]��iJ��T�����euU�P�`!0u�Y���#�mG%�S]��p+HU X��H���6Z8���;�0[*M�/��D��@�(V��4�
�[0�����PWZT��r0��i,���[ $�oFhg�"�)9xW��[�M0��0e��b�w�@'�*}��/5�{��f����}�p]��(�_�x��{o������������yV.���}�/,�J%�� �l�Y��,1����0J�H������Dvv��+�g���K����{|!�B;���&�,|
��<�8���k���7�jD�v�P�����]>c�D�Wd5�Y��3q������t�xt4d���rs-�
�E�*�m�Sz����X�#��-�`�S�J��-�Y������B���P���2#��Dw��Q���h�fn@4�W5���f��(��Z����
�4���[D;�,l�0�?�6N�J#n)�J��Tj�����I��c�R�Xqd�t4�����Q��e��P��&J
����Zb�,�b����Y����Y���yS�_<�V2�:gy�uPk��
K,~�X}��_����k��~�-E�k������ ��� On#�
�Pg��;!�]���K�zD`~�c�!��I�?��'!1AQa�q������ ����?xYN��N�tR�o��T�c��������1�������}�����R �%je���F,c����MT4PO�[f��o�]\�o)�z��'E��t#���p������v��%����)z��g����.���N��#S��b"�M5�Nc�k[����Te���r����Wy�%�g9O5~���=���3�M�!����������5����������#p�	h������
FR����]/��QQ�*����l��pB9��,b9��In���)�I��c	~-��b[Yt�Z�;yhW�������RIZIt|�7����������YtGlD$5�*���P^��a�W���.�~�2�������/�w�[���yE���[�s������PnG�vz|���1~U������)o4���&S����2�3��e��UG�J�+~b*����&"Sl�n��_Jk�|�S:�������6$S�*R���%�+���)Z#��Du
����A`�}�E0�QT@`�Q�.WD`�����4-���xw(�q)�ty�Z�5�$��Z�����)����D�����r�[�Fx%Ef�s4�Z�wfT��83��Tu�UA��w���?�b6��q��d���l:�+&��%��'�P.p2_^
?;;���Q��4�K�
b�@j\�O�����?IR����1q��I�Yu��u!\�VD���q1B+!�_����j(�M��u���-5Io���=���1�B��.T;%I��E�������:��
�O���	�	�Z�z����6Q
6����,�
F�d�Po��e�����)5p�9�Y�^��K Pt`��">b
���..H"��gPizc��l���J���U��q�q�i�!X��r��hdgP�:������&)�`���
�sn�|A	G���@���2��K�p�p�� Ac��Y��RZcW
�yL����Ma���p~�
!k������� �qe�&h�d�6*��jR[�*��.��H ie����Qvk�����J(]z��x��Fs���b�qA0�F�eU����P�NZ������)
VW-�8��m�q���5��NcX�H����(��x��a����5�T@�U��>H�6���-�4�.\�������`����r&�:<j8�Y���^EC
��
���N��K>"s����-E�;�Y���,����DUG7mJ�����f
qv}?�v��d*	����G^t*��x-��E�M
�	Qk�h�=O��8��7! ��������������KC�{�m��9j.�����\F����������������U��S5�H�DH2�@�0m"������[e�C��zB�+o~pDE������]�����3[�4<#�fk��S����m7��x��;��js�x� ���}e ��o�_f:����+U�g���@����P��:�����-�������-ET,8����i+q	R�&���Hb7)�=!�%����b!��"u��	��|C���y�d����Nm��`�c����Fb��\�bk���&�	���v�%z���O�LTG@����3e�L�"�1��p�D�j��� q.�iZY���;�vw�A���R��,�X�K�,��s1��m��F So�L��!�|G*c1�,������]w�T�m�!`U�z@%�������+�#�p��
i-��aL*�}gI�'�t�iS�+>����3�����-X}`�����iP����bgI�&T����
#23Kenaniah Cerny
kenaniah@gmail.com
In reply to: Greg Stark (#22)
2 attachment(s)
Re: Proposal: allow database-specific role memberships

I love that jpg! I'm saving it.

Attached is a newly-rebased patch -- would love to get a review from
someone whenever possible.

Thanks,

- Kenaniah

On Fri, Apr 1, 2022 at 7:32 AM Greg Stark <stark@mit.edu> wrote:

Show quoted text

Patch doesn't apply again...

[image: 1jfj7m.jpg]

Attachments:

database-role-memberships-v8.patchapplication/octet-stream; name=database-role-memberships-v8.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7f4f79d1b556..412d5bf92a22 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 8c4edd9b0a72..17d42fd949a8 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>
@@ -269,6 +286,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>
 
@@ -390,10 +411,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 df16533901e8..1c7727e49c55 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -47,6 +47,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"
@@ -1642,6 +1643,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 c263f6c8b9f1..4a7679e637b9 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\"",
@@ -1485,16 +1496,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;
 		}
@@ -1508,6 +1526,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))
 		{
@@ -1548,7 +1567,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;
@@ -1575,7 +1594,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\"",
@@ -1594,14 +1613,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;
 		}
 
@@ -1643,3 +1669,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 11c016495e3e..4ce72dfdf83d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3868,6 +3868,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 722dbe6a0d85..a28ba50809e5 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1513,6 +1513,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 e5a3c528aad6..dfcc8deb3a0e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -369,7 +369,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
@@ -7353,6 +7353,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; }
@@ -7365,37 +7370,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 83cf7ac9ff94..b25f6fd4ec34 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4619,7 +4619,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -4653,7 +4653,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,
@@ -4702,6 +4702,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
  *
@@ -4719,7 +4761,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;
@@ -4768,37 +4810,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))
@@ -4850,7 +4870,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);
 }
 
@@ -4878,7 +4898,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);
 }
 
@@ -4916,7 +4936,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);
 }
 
@@ -4927,7 +4947,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;
 
@@ -4938,7 +4958,8 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(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;
 }
 
@@ -5014,7 +5035,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 ec073e1ed061..42351636d395 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,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 a15ce9edb13e..435a39934c8b 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4082,7 +4082,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 a675877d1950..a692e6b9a4b8 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 9c9f7c6d63c0..4f723d07be9c 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");
 
@@ -1265,7 +1268,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");
@@ -1276,6 +1279,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;
 
@@ -1316,6 +1320,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 8a9ccf622107..3b2bfec62174 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2350,6 +2350,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 91ce3d8e9c35..82a9a0451019 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -210,7 +210,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}
1jfj7m.jpgimage/jpeg; name=1jfj7m.jpgDownload
����JFIF���	

			

		


	����"��8	
���ea����it~���0�7C��lN��1��~��a<��5����P�wD2����������]Q+�[L���F����_gF�!�Q������4�l�z���u�.�|����m����m�EH�L���uE�n1�_�6��HnS,�!��6;��e���.�W[j�Y����Z�X�{>���N�������.��lQ�]9(����0����} �@��3\�^,=w�a�xD���?1 ��s��t����D1����zu��vr�x\uv2��h*�^0�,if�#�0�t$�����h>"�r�6�F��+1���U�r]�=��v�������7U�g�G��'�$[��>M��
*��SLr|�����%�����"�_���0o��W��q���VY\�t�����o�F~���F	;�2�V�!^|-�,��$�k��,X��������[S,����2xt�Po�)tX���v"<�{���K�YsR��;Y��>�W�O�7��he�H+��
���@J��l��H��z�����Oo�������]?��[J�o�����]qC����L������P&B��I�eaf+���=������\V��<���Q,��\�F�RF����5��/��9��s������bz�h�
�^�����=���zT�	`��t�#:�?�-�����sEt�.D��\��N?��*j��Xg���P�6�����{�>|�(JXi#�C���@G�`�ajUn�H�]�N���hS�	!+���T����ZkN1)��!��#�w�N�-Nk�5z���:�85Q������&��i���~~�����N��\~��*�5������y?�!�����36���ni��CfX��f��z��E�{��9<��&���c�A�.`)2���(�m9�1U�x��lMw�RW�rxg���<hG�tm�e5���@:����{��J�fkm�����i�v�� s�FF�wyMU�V��?��F��d�i�WC+Y�=��r��,["t�����(��L�3*L����-oE�|���a������K)U`$
����YZ������+�U�-�o\&�&���>�v�S��/IZX��}k�tI3��GJ��{6E�n,���b71;���%�g�1�v���AI���8�R��f?J�c,V�l��5zF�g�D�&QvS�,sa��
�.V[!��"��;9'~%���`\�������:5�����[��n�`��$���)�\7!/<lip�����������uS_������,��2Q�0�.����������rz�lj�������Gq�G����5�d�=p+\���@w��D��dQ�E�M�2�[dr9���T]�'e�\e�fz{�����Q�P�]p���]�h[6�I��o(N'M�-�wK5�����9w�����]Q�Y�n9�#������.t'#�v=�}�v�j��OT-�N������l8(�� �k5�er����N�f�����PU���j���H���Q��9��jr4�$���w
n0����=�[�B�1���Y~SKS��2����+���������G��k����;+��C(����]4�A:T��$K�V�v�l��^�����oS-��{z�Y#����}(�����Tm���ik��zQ�_
�:�n�7�lVBZH��L�{��x���|�y�s���.�sB��3w%��;�+��Wa�2H^�i��d����G��6�r���#Z4��3;�t�#�<�8F�:�ZD���U?�b>�R-�H�������#DEV[K.�F��ml�(6k���Dj<�,vioQ��G��9b��p�[�9�pB��6��@cg���5n��r���8���`
���t��a}�;�9,��I�15�UL���|�>�����G�Mk~�I��>;�aS���>����K�P���}��oP�G�����q�)�Z�	�D*yW���� 2g7L�.�i��"x����x�?�7�<7���c���j�#�w7o��.��%�f��m�r�(���`�Vw@�p%d�Y*�l�-0�\gj�,��n��H�6 �64���^�Gr�	j��[+e�]�N���+�O���=������I����H�Z�T"���c&k���D��7����K��W�S|�:N|���!*��D|�q��AZ��S���MYv��7FWId�;*v��^�E�n&����!�D5x�D�@3�R���K�#���'\�!b�J�)'N������[]<����'�=������\��A�^n�q(� [8B������<KT0��(���#iT�n����Y����)I�C+3�z��T�7s�����B���a�����HUmc��BQu�G�c��5��n_;���u��Q{����@o������)�����N�+}���#��Lb���DMx��.�os)��?�{���5!�D� ��]��^-=<�=���*����tB���4�du�@���KB�aR�c�
�����&�������S*QAz8(�9��qu�N���/]N�R,f8��.Zr���~�������}�5��nK�gK]S�A����1��z}j4�����F���)��&�\�{g�_!N��0�$��,�7�wB��<�H���2��+\���$b���S��:d6�<AIQ�/���dD(�Z���@������=U����Ue���~������11��we�:��r����sG���5�����J�-&�wb���f-w����W����W��je��t>����(_���FJ�eW��S������;l�{�Dc�w%����X�0�L�+O��I3gh�<u.w95����/!	S�l�)("�s/eM�����3got�M1E��*�??��eH��3���#��!<dt]LN�N:F�3����k�c��
�������~��W�#�V`����$VX�K
���*�i?$�����?�i�yz�	$)a�����R2@5~k���x��*��J���ni�����.�u��-��q9BX+�\P�%
�g|9�R���VP�zt;uW�C�����[�����'*$�u�������������,_c�q=�>z�]:You�{���N�#����`�##�$��j�$p�]��e|-�aO7a0�l!��[2Z��L/�8d������x?W���CL��I]_��?b�0�+)|y���������S��`U�U�F�CR5��������
�lv�>�s������JSa-~�Q����y]����cL.�.��:�^�<�[���L1�a����ERvCc1_�����y'm7x%��Oo���+�Y�0��{��LB���C���k��|.�Ug�60+K�/d[���1�LQ��|���Gc���k�����J�9�E��[B��o�T��1���@���K���Z����^�/X�{1�������=���L�c��=�9Z�&.��$Qy3
��<�������U���N�`�������`~I�I� Ui,�B_M� ,�Ar���Z������(;�^�j���*��E=Lh�El�_�zo�>�4��JI�X���G����	n@G�a�l�`r��ek~����a���J����{�t��Us��x5������]:[ko�{�3BZ�g�!�\�:���~�'�����,u������J&x�O��IVP�'����M�4+�}���i��r���y��u���)G%��}(Si%���}o5��a����S�uQ)W������CC#	]j
T����5��u�[�K�PV�G��q���|6ku���/������\�cms�{�s4�X[H���r����H�v��t�SAM^�,��]s�hH"�>�i�C�4�t<�\8|(�����>��r�
J����� ��F[�����
�:{hA.��Q��m����zCX�4+-�1<{(����}
k�����s+�~�.�2�-h_(Y���0�G?xj�~�Al��2��E�#�D�����7��J?�d�`��
�����D]�M�*}l&I��'I^&�l��mb�����qN�������1r���m���:=0�'��,7���Z��}guF��K�J�dD�H(� Hm�*��a*�����F�U��7
�����dt���������}�H���	��6�#R<�(���6T�eQ�3]1�g�;����A'�1IN�$kV��F����nv���(��R[����uo#����#f1>���uv�<A���,�M7�������mx�����>��C��Pf�����V�;.��p�����dc�BS������c�%f�,\Iv����WH�E��R�B�@��B�JvJgBYJ��E�h�8��=G��B��{W�
Acb�}��X�������H^���>w+V��~Z�����'��������"�:�+�z��%s��;��!�df��2���/��{���g��R���8��7����a�������@��a2&*�����%�6���E�.�;�f��h������;����2�
����T���@!"5#126 $4AU%3BT&DQRa0ESqCF�Vc��.��OPCi"��Ob'�.�������5��W����F+�e]|�����O���S_'�l���������?��i��*�62��&�mME<���5�,�(�S�i�c��jk���7�eMz\�������4�+?&6���'i�������T���������X��5�p�jk�`?�����m����6ve=����7<]Mz^���SA���9.���`����5�z�eMEL����X�$/�S��`?�����G��S^�^#SE��������*��j5��D��k?
�cj�(�fv�m]M$N�������v�u]|�����Q���W_'�m���b�����1�*k����u��O����>'�]MF����������4(�h_��:����
� &r-������S�?����pWf��L� c���<=AAT�l���:��?�h��l���������z^yQl��=��c~�WUU[��I1���ih����6�j��j|>����
^F����J<�_Q���~��`���c�K[r���l/�M��������u����B5���R���
���u�������_�}W�b�.
�\�������W��CJ|O��6�v�D|p�Xf�_N�����q������X��U�g�
��������k�~��_�������1�bui���N���7$��{����N�S4��t���G[��\sU~�S����Z[����q_�r����X�����Y�s�b�J�w=N�)�y�@�V�u�[�r��2�-��%mYu/����[j>j#�������o��}U���k����{	,�lh�����c�Hg������A��Gi�u�:�X_�3Y/�Z�J�=�����X��U�g�
��
�d��k]+��k���6�[���m���Q��O�Q�����?������Z����`+������������;&X��:9�S�����c������c��I�$�ic��5��-�S��?����Q����u�����]NR=S����w�X������2/�����PP�������2Yhv/�Q?�OY/�Z�K�8�c]U�������������z���%��Z�_��V_���_����������_���U�O�j���M�&6�S�c�Wmqi����R��/��u�?G~��1���6������������ec]C�������u�u��u1�����[kmm���:~O��1�>Z�o���n
����bR����]��w ��%��q�k~����F��"?�OD"c c" ������ 5Ps��}]f?PR���R���3q*���bM���I���"S�8cy���(��������������#���SO�^'�\������������X��T��������I0�z��>Z��x[�u��'���R�I1.�i�{ c����������C���HF����:�[kmq���[F���5���
��T��S��r��#�G�	m�b��1���}�W�L:,����i\Z"�h���l�"Q2s%:���K���|���������CY@<v��U)���Z#��G����m)�#}*��L�Y����Q����k��K��Uu"6X����}f�#*��Qr����p�g��E?�9�+�8�X���5�1�^u�"~����7�x�a�f���/���Bj���QN�Q�k��+QY��t��J����q�C���9��J�]S��c������[m��_hFN���=�Q�Q�D0h�RLg�t��~��Nh�h�qj�q���_��e��~V���R�v�T�1xJ����l>�n���Z����v�C�~��f�k+@1U������4Y��j���O%^�����������Z��z1�����l���H�
,Au6��r����Do5�Bq��4�_ �J��I�����Eo��4�L�;[���Z������(L��xk�z���z
���,[�4U�)��{�u��Z����~���qg_�4��f����!��Wn�V6�c�r���z��\G����
���s����@"��/������O���4,�|��|z��-��|��I�w�.����7W���%D���=���(I�r�
Y��-�\u�����B�*~P7�:������M�n��YBH5$�R[f�O!������o��H�-/����I��[�d"�u��=<Fh�l��T� �c��WY���/��u��/�������r}]�wm�o��`Z�b�|+]z3��~�mY�u�j�i&x��?}s�d/����E�}t��*<���W�#�6���fP��X���=b��)%B�3������N���I�\�X�L��U����|MY����W���5d��'������i�^����"P�	��u44�6_�PS�a�q�z����;t�8��c� sm}���*u��n�S��H����� &3�T�>�L)�I�l����p[WE����������qf�L���G��s�W����yp�K�+X%b�����AK�B�R��6�P����AG�3/�"�S�J$v�ug &��N�.����v�k���^�e?����$���D��O�Z�k:�~"��]<1$���:��X;�E�9k3�����,[XM:��.9���Is��F�=B�����M����Wq����_�75��u�t������T�:Nb�T�����a����m��| `|k)$��,�.�
�?hEN���iRq����)`)�#�Hk+H�L���0�jX�"�����=����������>�Uc�:��R��[b���_���f�0eVp*L6�CY;�c�/�n����vu�+�i�X��(��V!y*����
}�^��O�z������NHHZ���Y�������Q�y�_f���bK���z�rijgh�X��c���s?�h�"�Y��
��
������w����AB��L���C�[�V	W8�:�����_N����{5����U0���C�^d�VM<�4V!���E���6����
�k��������-u/R$�Z;B~���:���X�)�5���Zj����.�3�$�{\H$8��Iz�����t��1�g��W�������������W$��9z,�U�$�OS��]^\����i�O���k��.�fc�1�l��+�Z�����(�X�1���O�����
U��m���cDu�9]�pG������g�
/��+���df�D���j���=d��k]=�z������CE��
�J������h�bJu���W�����J�aeu���D�����y�al��*L�+^l�r��Ugu����Dkm�G��3��
�f=��"f�a%�4�����Kn5�{�Z����1`�*���!S�!Q�����Kk����_�>����K_S=g���U^=5����E,Z���� aP��g����X*�c5���PJ� �d���n���QQ�(fryFF�����N_a���B��/�'����XS�����?�^K���K������Z���J����!1������*(uS��������rR�f	�\��_!���t���4�]��+��K9#��[^�=�SSU������[l��PFX���$����E��6�WH ����%6�*5P���k�1e��?�+Wf����"�Y�civc��#��sc/��=��+jiH
�;I����M-�c���{�9��������q�����#�q����
�H{-w��O/��:�������x��c�N�
�"�>;����:-)\4���z3TC$0�~�
��U���BR,U*'��j6��]D�E���8�-���1��#�L�
���mDk�'/�J0�?!u��b�>H�9k&
��#���*$�����`gQ����&]<�H���. 1���<�nE��m����\-{�h�?� ���9�,��DK�#�w�II�F���u<�""w�2D{�<u��~�,K�6�O�}�}
?�>��U���Y����[�(v�O��r%�Q<��&"�����:�����t�8,�����%��X��C�yjL�J��]��I�XZ����-��.��{%�3���< ��X���]jc�dG�����F�����;��q
y���������9����j�\�"nb��#(U����L�����Oy�E��G�9����
7����}�h����G����3�^?���;��i���-7��;��8?p��07T��*�!��8���Ug�G[|<�g����>����vz�:1���|���	���P�t�&;������2����l�[�uY��k����6��4����5&j(���������F�S�����a�u�����bFQ��,�fr�X2��{���k�sq}���[k����g&��#E��P?����zs8�F�e6�L �l���Q|#���?�v'�:����0�-���x����`o��,[���L��R�������Q���K�o!%�y |�E� ���;�~,1��JGr2�{ ��?	����iCF��f��uffkv�B
8��X1��X�Z����Oi���t�y��`q�BV[(�cL$�k�3����R��@@�����-� �a���������K��v����rH��X&-�j�b�5����O����m�u��V���a:����&��dK'W�&A/�r��APG<���[r8*���}�	���r�����T�c<�@}���7�}����y����|����~�i��w�[��4���K�O8���B��(�L�G���<�{�n'�:b&&7��������k}m��_i?CF���=[�n�=��|��^J����" wg��
6�b;nt�����D�
-N��:���#;�����]x�1��%��������US&�a��SI"������dOjx�+Jd���C��C����:��������V3#O/Iw�3��|��o��r����c���g#*�Wb���
�����?�j��,:8m�A�\���ff�I(&"�\l0�)xp�Y�7>C: 9G���e�;���3�G���< ���I�bU;K6����@�^�e?�5,cB?��O�{Db�81�~������~���V���L����)���E������8�D���9Dh�b0�{��� ������#a�;��dt`#T{���2�R�{9#2���5��wrx�KA�Ja�����qN��T�c�)k�3(u{Q"�u��uP���(���:��?��U-[�[�P#��E��X�I0��#4q� ;f;�W���N�-�
��q���m���;���Q��	��'�]�
f>�:"@����6�yH���3T�)�`fcJ3)�1�
�:_r$;���o�V�L�^�B���q�#��(�k?������������k�h��������.Jj���)�^��T^�2���q�KN$M9�O��HXH�@��`Oe��(X�p�h�a-��jc�vl9=���@9�L�����g�a5��??��U��)a���6�a�u��(���D�_g=U��5�m���:��������Z�Uw�c��q�������_9���Xbb�D��dY�`L�H����Cr9G�
VA~(��������!y���\���L��#��!C��k/(���w�Lw�+�����f&�F~G�o3��%:����m���v�m�����|,�|���{���a}������D�^5+3�]��i>#YQ3T��t�3dE��M(X��Q!����>c�G.s��A�6�������ab��m���~_v���m���������k�
{c��	�U`k;��V'�n%&����G+o*��t��}Jf]�c;L�s,V��i�[+�����\��P�wB*���LJ���<�T�C�eRc�T��w?�0�I��Q�9������C`�_@�w&�4(���-��0�10g��{������r�_��ru�^��������v�_��g�Q������r!�Oz"���3?T��u�;���Ue!�'Kx4�����85�#����^��,�`"%(�/�3��R����l� Q�Ka�\l2S��z�A����d;NPUhXC�yU���^��� ��g�����:.�c�'��T� Z��|hGx���3�d���\���������XD L-����[J�c�q�kVeg��Wj(�Ez��4Myd��T�{�
)��8�����^���\bH�B���N��AG~�!-����;fy�����}����+O�f��s��%��&;��[ko���}��j����!��-x�}��Z��[C��y+�YJ�)�J���"Q����K����d��L� �5	D�T����&�l����r����"��� {@2&p�)�}I����:�����X��5`���T3ZP��g��	��rGn��gU����k����!�����&'>t�#**f������A���m3Y���z�=$j9����y�N�h:���x���"E���T�����&����������Z!�7)��v���-�
<��K�rPK�B�-B������m��������t�#��.�T�'0u��Z��6 � f21:��)�0����LT��;��rj�,��;p���/����k�����HX�`	�Z����}1i�������4�JE�$�B9����m�}7>f�N�g�e,X��38Ug4���b�&fu���Sh��\��n���(opt���%I��hF�A�
?��e�J+�j��M �I� &ItpK���v^��YX���s��D�����H ����
���z�L����H��������FV����,[w�-�m��5?���6����w���������5��0E�d�%�e�/z{Q��mN���.��@NG#J���&J������I��Dwbdf`�-�����3��pmNbY�M�f�/6�%��5���4�������I�0QB��}��'!J������R�� ����Nx;���0�z���)����w��������?��F�^9G-���r����I�X�m2@�X0K���q2���h3���2�Y� ��b3��l�%4|�
K��7��#�����]�n��3`��,���Wbw�x��~��j@F��PG.���)\;���hmIsZ��DS�P���������,���>���W s!���T�������	A��������n,�@W�=F���ER�,�Dv���-/���k{�������dfb���	��v��;#*���79`�=MG�U���&�Z#cbu�8i�^�K�r��{�|��Q���s����?
���q�uu
#���-���(��p_�����0�{�Z�����������m��q�r���(���0����7�k��dD���D�!��*������[jbs��>?j@G�T�.�Y����Mv	h	�!"klO-�Ee�����=�t���H��������V G}0�����g�����S��(S`�A����;��vVP�#b]~���Ks=*��v:��o�����[9o)���EJS����X�4l��ad��p����h��R�S�z�!~����q�A�P��>u�g\������o��w��7�/o��t�Ay!�K"9�6Wo�k���FR�	��*����I�%���2Cl6~���5�R���>������W=��S�������������S��2<�`���c������U���R��$k�)���H4���G
��E�!�`2#N. ��-	Z�y������)�I��0
�z�����U<YT��hv��{����Z`E�w'9zio~N)���%Q�Q##��f���6�tR�g�@G���V�D��j�-�1�OXa���K��SW13m��'m���5�m:��MO����)�t���Q�B�x�-��$'���~d�
N$$��S����6��IFz��*Y��U������c��0��(�Q�#�^�X�Q���#�2��
[������v~j_����Yg���4���S�$Dk����K���w���p���=%|�}�]n#{�G�(��J������`&�}�U�d���y/a6L����`o�JY�G�;�,(7OGy��^e���������f�eLV
Eu��aL�_�~�W�1b��uj�r�6 J�(�n�g����O	�9�x�(���,����p�x�1F�g��o�G����c��Hq�%����C��� �k���dX��n���u���j��6Bq�UJ�������!`���n����z�\�l?�������e����(�� t����e�Jv�y1�
�ko��������I��p:����mc]�X���m!J*�����n=iwb{v�qOy��-Ib��x1'c�K��+��CcY�3�K�1�����~�,�&�G���������"��b��1!�ml���:�oE���H�K����j�rU.-.S�=�efG��j�Q �vSd��1��P������J�1�(bZ�r���T�2�(�'���i8q�E�j��?�L��L�9_�{��<6@-���I�
�����=��5�+���>s%�WOa0c�7����]Gx���������z~�������Y�,W��k��f<j��8����V@+�r����$N���O�|~������U��3��r�X�7�*%\�|��������[��>S�������)HH�"ci����yL��(�`��0J8e��m��+���a�S�d�$
��
FgU����j�����c�v�Ck6b����r�!T�c�e��{��$95��v�+
e~��>Q��c�{q����v�����e��O�������c�Pa2%K�%7T����T�V��2u��s�{��v����k}o�_����!�1�DI��}�C�;���RW���<��q^�T��Q�92�3��}cq���$���
F�	���:�������j����:��NcE������J_/�l�.]���3�DF*�F;��E�!�^LDM���-����[5�~MK��r�D�F������&�W�UD��
V!5������-�>�����X�������U�`"x��wMQ����n+��[�?O�� �qsQ�������Y��	j���1�Y������B1Yp�N�E�?re����	�<S�/�+v#���(I�FCrL��;�k��
M��?{�>��-i�\M�M��/�o�=����dX*�Y�`��kaD������k��1��nH�@G���k�W���UO�S��gv��q�1c����;��	��O��� ���_%r�f�b�"���b�}J;Wv�E���RpzS���u�ut�	3�Ai����f�m���6*��������}�������U��3*���+`��xz��������AD0��$�o5T����K��4����"=�Y�m[��8�l�_$����������m�LfjNN��]�dOt%��95Q�W�m~,��"�br	���z� ��?�������q�}�Gg���h�K�R��X���eq���%.Fx�m������k�W��>L�gRf�?����8�><ijM��a�}AKv� �7��0��)���r`E�D�����Yq��z��&��CW&���1L��}�0
�����C�I�� H��Q�s������v0=�2�UPH��[��]�0/�����]���"Qv-A���2������I�R'������e�J���};-���WV��[��+�����i���x4r�8��,�8�W��
�{�&��y����.��F��Y���������^�?����.��[���.~�������%4Y�dlc�W����Hm?	���}�~����&�z�0a��9j��������5���(-8�,������,Ga��[Z�����w$����^�wal�����c���Mu����v����,�k}�r������l���\
���|o��D�H�q:N1��n�k��W��!���j��\Gz�0���4�������)u��(}��NJ5B-�;�1� ����r�|�0��g|��]�U�[�����_#���8.���?�2e ���=y�,�����D��%�����N)��oo����W9����V�=_����f��qb�����s��!�,\�g
�:�#c{��F�uW�&�"(���8�C_���y�n��Jm�U�	0���b��l�l-�����-��a�o�`�����&l�p�>Z��E`AB�=8[�j�6,�\�/!���T3_d�+�#�3� {��g���s������0�\��L��d��5@�X����?/�WPc{6����������@��uN6r�!�������_������8���s�I�r8f�q����<6��>��Uw�_A�u������_�������y
)d��UN���W����&;���U�s�������*o���X���!��d`��+WjZ��%Q��ky�c��0�����^�ScV��`x��s�6C��0'r��B�\�����|p�9'��3����B9=I�����l���P�R�[��AG
ewlrj�\��d�-���l\G��� ����l�$�F�rKn�B~uS!0���Xb�d3Q@L��:��Q8�K�QLT�7�������p���v�[�Pk��;j�:W�V���X*���YJ�?�x���^��QaK���K���}��O��������{?��^~j��k�����6�7S��6CBj����F�I��L�E��L��Qz���r	fG��G��g���������������0w%����]���U%]l�L,�����o���tr!r
���3cs���>"�I `q��E������w@G�K�iZW\m����6p����������YzKB!��{.�w����W�l6-�����LWg���FN5W���`�P�g�$���A_�m+8��k�\��;4��
9#����b����
+e2�E�q�pwzg�������N��)��C��n�m�����N�r���;����V�����j~?�����[���b���\�C�����������(f��eW����T%����
>�f�p����������\�082�8�u��X�L���{���f�i\�=�vq���d���B|������e����r�v��v�U��N��Q?�%����B[	�m�d�Z��I8�{\�I� ��W��<��0u�<�z�~�In+�e�����Q^NC�����M�u\�o�C�&b�q��<=���J��:��&�R�*�+O/2��
l����I��O�������	Y�[�m4��>�O�}����?��	�0~���X�����L���n[������?����z�����g�&}����}G����}�eW
��,~������4���;����[��}�u�](9����X��
b��pm���-��]S�X���e���.%2>cW0���O��o��������y�{u2��(�?��*��I����f�l|���2)\�
�?+�
<�h'��'%!`&Gm���X���0��,��L/�R.Bd���x�B"#�^��C<���{����h�������v�X������aV��5��*!��-�h��l����d��A���j���XKp�GS4#�V5�C�����tT�F>�����~��������X���Y��[:c���U�z��^i�1�}�<�a21����,�0R`��?I_�E[1�5��<����A��7��b&Wnd-`�U3P��G�1�4*Pe����t�9���c�_�����N��o�O!*�X�.����lk��L��4�zn��dSN�n���81'��^�b5���Th2�X�2��Y�L�'��31����}#�g�}�t��K��	���K���m�c��4�����=���<F�SY.�P	Z�E���uO�w&�&�mq�t�*!�&���;�U�C2};o��V�X���o�-�}'�Tq[�[Kq�V�����M�S>��B������
�B�d�-K#1;����r��2i��}+������u��<^:+���������?����v��=S���uL���V����Kf[���A���r �`��~�^y1V�Y���:�����8��Ylj������l��j���k�p���p�}�*�z��p]�_G��o���7�&>~���f�;�Z�8�I����)���t�T7�ZQ+\��z�xm��`��7KT|?�����z�/���3��u��n�N���u�e��[C2��>��)�;�����>jo��e+�53n���Ub����9l�J�SBt�C��Nh�g�u]�nV�J�������S��v�N�,�O�:����v��K8b���~���_j��k�:��Fg�3���3�kk�]�����u&r!��������Z���r��O�=a��xG����������cX��U�o�.?�G4�e��E������~9J��&H:���L>���mi=��������~�����G����<alN�_5^�%V&@��S�T���c4��S�i�������*]1{��0(!�t�k�LDB��Oj�W��(��)�Z'k��D��e�'K�Y(��td��5b�m$���k����w��,�;������?���������>k���gE��man
������-S�U
 ��2u&���t�GR���Qa�H����>��u��k����~��|3oku�;
�_���3�� U�S_�['��i������?���6�w��Z������~�];��u/�,���M�����#!�\�����7������n��?V�yX��T#N�P�
-Ew�����'\5�-�Z6hV$�`�uz����u@���N�����tpq���Ym<c����[��Y��y�+�=�|�D�O���������Q���������m�����d�eJ�d�K�����Ms!�qy�LUw������c��:�z�j�DMezz�F��0b�����R������x�G������E��>v�Y/�Z��~�(>���:.6���?�X���]S	�,`�����V����t�C��W�9�{2U!*8���4�W�-;�2���+N���c�-br������k�B���a�-����f,J�X������]�������.t�/\�5�D��r��[n3�����r�����'�~��|k���[���_?��VV�^&��A����	����j������I�o���j�\���N�w##�oQ���5u����Q��������l����2q:�����f�M���L�s��j����	�\���E���50��unH�-���[�mt����$��U��P*JU�NK'��������"8��s���	B�����h�u/�8�Q��T�_X��d���(D3�UfZ�}��Q����q�F�`8�q�[��AUi�z�&��"���M���i-uFW)I����Un��p
kW�e�����*�^��{5��y�]��<���Ul6���x��/��n��|��[���U���~������t��X���,c%��@�#jh�H��f\����uG~���DX��q���	�C*�����Y|o�_k)W�ti�L��"-k�o^�����XYVd��2
�U�r���/rz;/T�V�:8|�W)>��Z�d�U6Z��X��(�p@]?���=�J����y���v�pr���� K f�T�z����:{5���W}F.&'4�Y:n��(^��"�,~��S��c��s���$wf�Ge-@C)�u.����6X���Fg+��F�����f��5��I���]|�����AK1����\�3{�(����Fj6������-a3Az�hH���T'C���/��7mx�h\A����B��z���r��;��}}�~�����T��Q��F���.go�G��O���v�t�������c];�z���:Rq=6�FL�d�2_N������(�E���k:,�U��:n������F���n�k����:���hQd*�����gYQs4R�q�\VF2u��+���W���y��)�id9z7w5b�j$�X>!+B�(�����9�[	Gg��q���[��U����,`N�����G_���W'Z��U_(fj��c_��t���!��PG�[������33:���1:���]�/��R$�\/s2Y�Q`�1���Mx���>
T�W
Y������v��������]O����S����+�|������������Q}��s����&*�.7.�m��2�g%��Z�4!)dq�e[UHG�-M�8	N�Z���Y:���nt*����:��X[��f�1��g�����Lm?��9lBXXWI$3W�$��Z�X2�fw��-�b�,�I0;|��)��Iz":\���q��f��6�s���U(��r�'�������%�d��k]'!a��Zd��N�K����cy����q���^\���u}���.S��{?v�.�-1��q��m������R���FMr����Q�_�.k9���������wV���g�LzAE������Z���=u�����4Q��� ����|��\Y��F��k���c];�j����*�����`o�}:���8�������S����P�R�����b�&�-�[+q��J~7��u��R�4Sd[�R>���^��b����"^D��
z����t����C�����b���wR�KX�E�����^�=C�!�BAj��L��Lhg��
f5��I��K����J ����{7����7���<oR�8� 1`w��d���5�5��*�im@�.a(Z\,P�|2=��Q>�is�	����KBk/1�����h���I��3:�6�G��?���%����~�9���-�t��kk?F���UQ��O���NK���Y�������G���������(�f��%��Z�R�i�7��u�X����QV���f�\�m|>x�1��g������_��](0r#]A����m�������*� �@5���O���cX��U�o��b(�^f��,�2N���X��������JYk�jJ�����N���d/.&�v���)R�Z����Z�T}.���YY+�U�5w"�p�yZ��[�>y4�,�02\}�$��J���������Y������]G��Vg}y�{m��&D���g��h�"�/�:�1�_�|��]��Tv'��Q���H�q�f�Y��Pu!���7 ���A�N%	u����O�sH�������������/F�@��l�����q�M�E�Z��q{�1
�ak��Lq��j�w�5�v�����Z�����8��S=���h[�2���_F�|�r�T����P�V��q�_v�+V��� ���{?h���5�G��y�F_Z�A���s9�m�c�^�3���&��[[-A�AM�v������������G^j�JPfUs�DWR"�����[Yj�Q��z�	�����o/���8���R��\�������(��t�O�C����f1��-u�w4���f|��
��Q[BG��;*�.G��xf�-�#����{��dh���_����=�I�NA�!l��CR�G�1�K)�jF�������lek�D��6S����H�Xze��V�>���E�u����������9��3������g�~�Z�����H�TZ3�k�=���`�x�6�C
Z%�^Z��G�m������c	����J�ny%n��T�K���of��Q������<�z��v�}�F������.�Lv������V�
��s�,M��{m�����d\�������n�T�lU�8��#����8;����)j��_�$MJ���>d���j��d�m!����K�!��x�J;�:r?���ALQ���b�����s���#gs0��)J������t
�������:�w��r�;��]S��s:�?����@��2"���r� |����c��	����F��t7mB{0��W;��2�����Y���s�B����A�����o�Fwm�v�?�g>v��frK��v�ueE=��k������
V�&��'�9��u��uQ*K���Vz�5os}�)��&d��U���n���~��c�sS��������;�?�������oy��B^o&����-��������4����98G��S�fs(�_;S:m������c#p!vls�\����'w�3��/|�F]c"h��������I6=R������}���Vw*�U��:�<���j��>�+�����+.AsmGTgx�z���k'��(�g�q���L!1A"Qaq2B�������#0Rs� br��Ct����3S���c�4������
?����ht�������|�����Z�KW�j�-_%���5|��/���[��CW�o�����5|��/����CW�o��
�_���|�������o��
�_���|��/��E�;}���_!���v�/��E����Aj��C���Z�KW�j�-_%���|�����Z�KW���|��	��K.���p���������s�������I8'cx*&��.���gr�^3��N����-�R��k>{��l�qY�w,��b�E�3.�����'z�����F��cQ�2g���|AEm��VG�5��ZW�����K�V����#���hF�����4p!]�	����&M?k0{�����]Ag�qY�w/e�9s��/�Y���M�,�;�,��S�	��jhGwpF;{�N0���a��O�rgL���y�&{��G�e��pr'r��#;�!��F��D�(!ja�G�V��������_��
~�f�QS��\���s�,���x���fY�������0�/�Y��2�xFN0����{�N0���a��O�r�P��nL���=������^�q	��k�V������A�L����\e�d��#����N�����+E�-s	�
����
}�.{����:)#�,��+2����g 1���r����+|'�&��d���7���������'/g~L�����'�k�����9�a�9� 5����r����+O��q��vN?�?0����������F�(�*�gh7���`�t.H������8�p	���7a%�gr�^3����%2���d�\V|�	�U�V��d���7���������','�����E6���b}��6u�@H9w��k�������+O��q��]����?0��e6�w���P�������6��Ct�U@�JYw����7��w������ ��M�km
��9+�������o�\i�d<:(n�h�;p�EF���E�p�W������+jA����v��D�I^�>K�r�c�~1�����p�t'>��������'F���5����P��b:��|Ny�X�{�.:�4`2h_1������P�=Y��=G�������1�����t]'Y��~!�����j�/�=�����+7nm5��U�����i�����������'^��7�R�����6��<�o�p+��D"����	�gLz#�����]��D���>����7n�p�	��3G:�EHZ��&7H9�A�n���g
������'��5�����oN�Oq����fY������s��J����N���-3�����[6�M�JS��l�^	�KY�?TA=_�B��yy��^���oo��Oe�9!Ds&A��u����d'8�N����I�H&A�U�q�Y�v���G��
�=H�G�y�f�U���A�����>�o��yg����-����c�8X�/�*�|A�Vm������s]�6m��\���f?t.0����)�T�d
��x�H���F��5-�.��G5Cp{w��n-X;�N�E�aQb��^K3��������B ��������^��r<�&���U\_�Dii���Es��N�����2���=����L�z�t,�6J����K�8soO��M��!��<;��)�L$�m�����oZc��GsOZd��wBkfG��6��x#�K��B���
����|&���^��r6;�g6����9����D�:��sOG�Q^'�ki>��+�k��TYk��Kfg������=�=�i���l��^3�g0�M'��q0�'�FDN�$��z%�2?�f_����,��)�������j�2konF�{:��9�Sw��������F��E��?z�Q�a��@p�H�B�f�]�J�'������"ent���!����bL���fY�����|BGC��k����1��`{�b��;��E���+4��.km[�,%�#x[�r1�>���	�u+2�2^��rDqy��y�P�8N����Y��-/^��<ME�W�Na`�h��]~�<8Z�C`oR�9����|F���)Zn�U/����vO���6g""��7�6��`�x��Um��<THV�;��p�i
br!Z �EE�d��M�4I�W��|@L����Z����T!��z�+6n��P�H:`k�=� {.����Z��I�����":T6�s��u&>�����}T�l�JK�2����p�gj�}��.�X���r���7u�u��N����eC5�]��}TG��W	����MW��-2�6d�Z�9X��E����/��.l6�,6�s_/$hM���Cx�7bV�����9�[���>�o�����,=��vl|[��j_[U)�(�>��J���VlY������6�
���l_)�QD �i}������.nq���C}/
��W�}���6�K���=����!��QVR��*�% �(�J@�'%P4|����v��*o@�������9�s���vG����u��)��9��������$0����L���tu��T���(�2�eruC���{����c�Mm�;��#���e������hu��~�b�m��d�rQk}���7"���5�Y���5�t��Uv����]%t�z��a*!z����`�*��)#�]z�Bc�������"����������>��h����7:�.~���.�E99�{�vo�\5�!G��b�lv(������3���!���-i,R��������1>vP&���h�I������t����W�Y�D���n�B$���j�,�h:v�
����#Ut��&����d�+��nSC��U��x�|.�������hD6Fv7u�e�]��F�7'�����$�1���a����*@������J	�ZjK.)��y�Twh�<�gQ��`�����gb��
���x���1������E1���x�G!P������A��_��B������n�m��-mT �^���-�>J��5]5�U4��h�j������1z����L�u�6�O������>4��=����Bp�v�&��m%�����`�m�'?4��"�l�WRv�F[N�1��=���u��
��	��7L(��d�7�����1#j�-V�[���9Df����:�C�bx���Z������U��Xm>����4����jP��4�����L���Uz"+T)��]�k���pS�t�0UWLS�0��g��M��'�F-��Nq�Q5�3Q^\��:@�c?D���j���2���^�0�2P���L���4�� �����f�)�X��<��#�R�|�
����^�K��4��w���������SxK��0`�t����=Z^�.L�t�!���	xi��1^���(�h��$���/B����n�(jN��`�zORugt��f�ek�O��m�; ���_�zlCbG�&{'J�Z(�c��;B����:Mo�v�!��~�<�+�(dI����p�i�;��6���X�$\fg��S�l�
k��8� ���=��7��9j����l�g.�j���&���* c�����F��w�D��N��	H�Q_1I�Vy�c0IL���8L.���N�W#�S�X����>��x�i��R���m�;#�l[.o(;8��,�/L�;���v���\
M�a�0G��j#'���a������������5��������$3Mg0M9'�)[��n�(���9'hB�8��a$��}�B�v�;��t�!���r)����4���F�xmv��M�`i
���-mS�1��tdf�#�D2����*��+X�L���t�j�2
W�:���8���A�x�|���~)Drq��6U�{-B��D5�?	�P�������iQp�x�	�R���zB������M���[ ��W~��l����g9�8��
�����	�l��m����8�?E[����g*�IsO)����7�S�@��PS�j������`�]�����+&��i����������@�^w'��$��(��'QE9D���H�z���?u���vN=��{�p��4��>��9�7�:���M[,�h���C���	�.YY�����Rbg�������;F$����D�����ab�o�Q*���P����y��Q�:$0M���f�6[T =�����h�\��y8�=j��(�����oj���!6����eDF�979�e��"�M�.E�n*]k`�&���t��T���dM0Y���������~7&\��9�����@�����7W�=��v�z�7�X���P���:ZC����&��T�2�b�����Zd���r�Li�%B������y��lM�6��S�7bl@��^4P��v������	2~��[�t��6XU��U�������L^�8kL �F���-a7GJ2p��3��f�j������0���0J�'�����|��P��V����4[����Z�m�Tg4��v��F}�G{�"6�oE���
#�6N�/��,�;���	�����Q�rP���7�Q�Sf�8E�����+]u��v��j���6��Y��K2���������1��$.S�%��������gX��)����c��rl�����RT�E(����@�e�������;#�hG���9	X���	��Cw��J����D6<
Yu�B!l�����C�rh]�u��"���@<��/���_5my�San�	�����Kb�\7��3���IZa�����{8F��J���o���'WG�e��{Is@�8`�C-)�����rMmR����TE��T�'z��m��h����!�~���D�M��so�>��7�����S������p�M5����:��q�~�E�>���b���ku�����JbH�_��=#�a�`�!���y�7�"��[�o���?r�M���U�k^��.�hP�\d���{�:6(�$�"�#x�S��[�5�p���4Fg[���hu��l�V�����55iE�t��ZBh6��S�
�d�^�����M�ud��7�����3��z�������9��Q[���m���1�)����Zt"m������3?$�[��%�w�������l�J�}���2�g)���	�"�i7�"���bVH�yAE�����M��I> �m�LnpC&���Q��,�, H:]
#��NI�i`�.Xu����v	�������5�Va
����Nt�5�t�;�2��Z����S�`N2�i�z��%��S�p�C����m�?#�|{S��M���m1�	�w)����I�0y���M��3�OE�h��C�8+O���-J@�x���I���h��b�kE4}��'�6�P�;���i���p�PZ��8�Z'I���
�����������Ti�5�0;���e���D	��A%>~�#��>r9M�����'C>d��H��a<�v��lR���'�H�(
9�<�o��(`1�����������9�GZ��,�?E�|����e�����q�Xk5O�D��s\���c�F�$~�
��7�n�����2r�
t�I>m�.��2�B$@����)�8&�T\��
���
��D&L��v�D,�5�����������8��6��"���S��d�&�m:X&�I����.��h;���e�F�X�r�NG��������d��������G���B�`����S��'��-\(�
�J��Q,�6\���x�|���G����Im!r��
\��Y�� �M6�F�"�N�ik�t�C����2�<V���
�>��|���sv�� ��l
��wj����E�b5�5�B��q���e\:
q��x��9�s���@�I]q�HV��w`Q[h7x�:����c���h��:��X�
�_��b>���v�ji�P����v������n�Nt��='�]
P��`�����-�����?'��S�B.6��4L
h��	����d���5�9
i�]U
��Z \��*�)���_�7&�Mm�
H����@��������m�����1Ts4z$�bVj5 "Eg��i��kE2C7�K�V�����7�i�B�m��/�=
0����R�J8��\�J�F����i>���l8!��C���a���d���]*.P�S\1��?s��~N?�?0�^�8a�:W'#k����,S4����t"kr��a!qS&�=��I��ql�$�c51B_h�a��� ����fM��'�n8
Gz��}������P�nNL�?���#;��f�^��x�P\]?��ori!�9�-"���0�������>���'�?4�PYb�Nc��k`�|n;T7Z�����l�$��o���B4��;!Nq���2�u&_[������}Q�j���dIV�E�	���:G���tm;}&7(n.h�!8�C.�a�\qLqo���a�&��5������1
��(:�D�S������-1��L�X��M!��V�<�p���B"$'Z���'7�	��	�(o<�E��_i�A����m�=Q�.�z�����G�u+��A��MO����?'����`cT�UO����?uB�����B` �4���g���AsK%}��J~^h�CN%F�`��:T<�G
���6�7`Nqn��U7(9��H�)��'6<�l���rp��f��s�L"�(�I�I4������4���3�y���;F �
�����M'��a��bFq���ued��h��p�6��������~?�9
��"Y�P�5�QG�h��T�����\�-�k���T�I��HK8/�*u"m��zcmS��8���P��-�:$���9��N�{=P���(�!�i��� ��	�p��e�t��H��&�S_�B��b������"6���{��Lm������*v�s�ia��0�sTHmx2����iA{k9��&�����	�.�P��n��]�5�r��g!����|����4�"T"�6+N���+�F*����s_)����S\Z��$����n����WQ8�R�bm�T9S����j$��[�lM��0���qv��iQ)kA0��5!Y_6�Jt):�q�m������N���D!���8(D���`��~�`�TL�i���8T*��j ���c�������:��8�	�:��NuEF��_��?u�[�~N>�����Q�#f��t!���X\(��)�����#�&&���H:�g�������T�[�P�%�R�6�/D=��U5�C�y2(��
��S�j��N�ip���7�$p�`~.�`��7MD�X���%�2N��V��S���pR=���<��C��v��7��)��
'�����\��aMh���|��['}�o_��"�s��HW�?���
.��9��j8G���/0��=�8u�[�~Gp���NuW�5���^�< \6��Q[a��~��6� 7�ji�pVt��Nm��x�M�A��)��Gs���)8�%!���#5Q��KUN �F!2?
��D�},eD��FX��t��Kb�b��w�Vf����a���X����U?SR��92���'�#O4��)���M�Vh7K�
�'��&�C��D�)�S��Z��B��.��9��'8���;�j�n'w����?#x{�������Y�4SZi�F>�i��QCmTX8�S��^Xu8��d��dK�"�
�%�tu("�i�&rN��zt��u���DA�)�9#�?f���\�*jP�{�.~ls��hY���N��&��J+��M4��t�N��jQZD��T��O�C�)v����5�(0����&��Io)�A�F��LK�jM30��������^3��y���F���O����?#?hG!��9B����y�Lhg}��!�!u�
����xv	��F��~�������;S��n����c�]�A�F�j�Z"W�R|6���F����ut���.l����#s�������V�����D�J��Z�����c=.D���
�6H���)Z"W�Rkl�����w�cI0�g�W����.~�w��s��k��Ux7	^�����l�JTp��5��������-
�m���t�:���o!�������e�����;v�}�o���W���L�sWt����o�M�Ln����p���H�N�D�����9�5��m���"a���hxu��-Y�w/g�98<Y4���N�+Z�Jic�k����tA���C��	��&����?�d���|AfY���q�/������Sg�+4������Y�4:��Ck{��2Q���2>�b-��g�L���J���2$8��N����1�_s�[�~N?���
����,#^��f�jf.��u'�`umO��%��2����g'	�l����<%�TF��?��:s)I���Y��'��6����Y4?�h���,�8�o.sM������1D�w�����8�6��=�S�oW��Be�y&�4���|�h�H�P�4:~������~?�;(�s������l�r��W��Va�Ru�C����,�;�����s�6M��Zi�n�������,j������k'�d|�6r���L�V��<�s�u��"B�ZmL�����l8��ELM<��-FC�`���X�229�'�����&JFS�+X�O�p~
�%��jc����������}����Z"�
b������J��N��<P��P����]����B����T�����g�M�@�2���6�~O�Y&��Z> �a�"��\����,r���{Z�6����;�1P�D~�����C��������/<�(�X�N�f:��M�O���z�D��dk�t��}�o��8��?hUpW�hCZs����M�����)�=������:����2;/A�:�Fi�3�%�t+$�]�%q�WR��hk������erp�>��m�'-]��49�gC�be^�h�q?D�X25��*���y*vt]ehM���T���+.i����[@�O���0'9u��Fn���j�n�k�5�q,����zh���#��������e��#� ��V*�qB�/q�[�~N?�?0��'-��;���p�E���u�:n�@��z-�P^�	�@�<���0�@��$�i��g�k������kO�w���[.���t�\��N��-4�m�oOm��k��:�D�dj�q�P�X������6��x���
��Z�M����\*#X�W�Z�&�h�������E���)�o�L&�����.Nd����`�Wm�zk�H9����v�����������d��asd���;�
�pu1gWr���Y����m7����k���T������+�,_
�oG���������?j�?�d��Z> ��EM����/�37�f_���'^���J����M}��}G�;���Y�w"-J�4M�m�R��6�d��Y��:�)��+Z$w��d��7J���e����^���%9o�p�*4XQH�jY �)�m1u���ak�W�@���������=T6�"�=~h�3#]���)��}�:��q�����\��mr%�C����-/B��E��$W	��r{��H�Y�
�m1
�J������bB�����,�M`n�:�������D����G]�2E>��X��2�|AC�t�d��E�^�ly���~�P���A0�i�md��0�/�Z-��H?��a:]K�0��|�����D����+�	���8�^�i��5;�hOX��M7���SN�KRoB���OJ�;6N�Dy����1��\��L�hr����+�y!��,������
�=���D�A������fY���Sh�SN�R�#��Tj�Fu���k^�x���#��P�HN-2h��hy�J��2Q[1##"���N�VOi�2��!#u���O<��]���p�)� �:�m�g	�����td2h� �F�(�R���V��>�����~�rK���l����K�T;v���I�*'<Z�	�l��!�
W�����r����,�B�7Y�x9 LZ
@���[�5�oqZ��|A��Y"�s�m�^����m���,�;������kf9St�e����Y��Nx3��uY�w&��U�d'���%~�"�,�xu
$�9P����$)���7^���T�z3�nl��B�x98��w���R����_�����hL���)$s�l�,��_���$;-u�~���n�L�"J���b���M�>��_-�E�	�������|�z(0�����ajy�$5<����Y�Bn���2�����U���[�h��l����:���|A2Z�a��r�^�iM~z/��E�4-Y��z���C��Vt���w�so�����	���[��������%�D��[���M�)�V�pw�v f0��*v���U��zT���K�>��������
�kd�
ia=����e!&/����w�+��0���	���G7	������D�nR��?c�0��7���@p4��{P�o�B�>l�1�����B�������!�!��a���m���+M�F�nq���q8DX�~n �k��v���
K-|��ms�78�Z��7�����a������4�������a��@_�mT��5f��Bu�)�Q-�5"e4�� ���z*[�����.(�<Z�VYp
��^K���8n����1����ge����Z�F������g���j4{U���g\%v���?���-�=��5�WaP}a�n�!�
���#d��o9��V���I�@�C�K�''��V��6s�vX.h��bc�5��-:�����N��ZS�}�}E=�}�o��L�O��f�Ew$#�o�?�L�B��f��|�z"g��=���'���R�Z|����}v�D
=�=z
�_�'�;A�}�R���Hm�zm�AC7D&]D��a��)����+A�O�m���`�
�[�M(����J���������B����+��g��m�G�7�+
�����'!1AQaq��������� ��?!�`�e_,�E
g]F���!^�,|����ML���O�!n��I�C����~��8�Fr���C3-o��
��o��9�[�yn�����f8���k�'#
(��s6y+Q�6���������&�lQ���Ezjr��r�@��2�>�����>�����������~�i�������p��jP��N�k��H�O��Q��d�|'����=rkg����'�����}"%�����]@�p/��SJXU
�c#�@�\0���M�O����	�C�FY���������5����y�}1P���2�~F�z�v����g0��6Bj�7�����Y]�������~��xUG�����m���c;�{0����g�Pa��<����*��J�	Y�V����
	S�������q��]+B�C�|��D�:���S��;���#
B���1I�~3G��I�+p��Y�}�`�Z�����D">1���(U�(L����hPD�:&wx7�}��������t����}�:+�Ty��:��Kc�Q����91�L����f^Y�-���]�/Q�Y2������*vN�,��TN�3M��n���p��.}c�Mi���X��U�8���l����O~�.���W�}�v�������>���/,��A���q����7_y�&��s8=��+�/BYlgo<���|v@�7�K����[�1�=���E��ih���=I��o�r��=�w�z��.��_����S���iW�'E;@�e�9�v)]f�)V���:m`���M���7(����/���C�ha�\������[�>��EL����3����%������K�+a��2l}O�����8�9�������]U��h �M�R��i4y~<���{�����AUr��r���1S<2����$��d��p��tiV�r�E������p�]��W^�0��i��2�
y(v�D�}�tX5���P��`P,��K�"
��`�,�*}�L*��J'�t�b�0�`�U�Dx�|e��e��E��3�
�B���ap���Z�����fk������V�y�~�!��J>���������b�Q���������b>�+�r��
k�/���`�i�.���aL�<���TC�R�,�F�$��i=����]9G
��/�1$���!*
����� L=��X��,�VM���Y%3�lb�`�f��3IS�4Yqg�DD�k1�!\���*��hyH�j[���n���b����A���d0%�v���
�.��k^����O	,��y��q|�
9��f���t�?�������(X,�_�=H`��s�2���iP�I�~�x���K#��=	K��$���xd�gN������[�|���iB�:�:E��B�J�w2F�(U��2��� ��']4�Y�i,��:B$`A�b(�M����3���l-H��s
��5�����&�hM��(m'��{����/���_�:���Q��y�L%��R�Z�6<_� �:5t��PX�z��b1�C�U4�����h�V+GH��Y.����%�]�@���A)c��(�Auc�6���K�W~��oY���.�g��1��y��XC�G_pFX��Ry��
���d�;�����J��@�5L;my�~ ����D�i�M�q]�������;�}
���Dpd����]�(���y��|�:�^��n��>�` ��3�Lm!x�g�.2�~=��O���QF;����x�����oU(���I����t�2z�+�n_���ka:dasJ�y��/�^x�Uhi�l��+�������ja$z����t���7*�$a�qZ[�c�E�"l^;��B:]{d�u���]�`O��������(�[���4�3�~��
�V��K��=2��z����zn8Qd���`���D��Yc2�W����W0I���0��,���!��=6�
M>c��DX��A�K����wN�*���G�$c
:�=���@6'Rd�^���~��D^I�6��V�|��QR�y@���#��QcsL�g���`9���P��'���0���?)Y9iV�XE�M�P��5`%l��v�m�
!e�������t�����_f���F
����������@�-�0X�>�m�H=��m���C�Na���>�f���>��jm5������g���1��m�s�r�����)�K���m��G����J��~f.1�������o���x�dz��������tuC���-��}�	@2lv`�]���h�mj�*A��:&,����������
?P��_����k��U�. �o��R�<P��]��\?t[����
�p�GB
���
P��MAm�!Yk)���>��}_��mc��pZ
���'��ad����{�z�c�6�����
�w������q�P�P����s�Mhe�c>W}#��!1�X�4�v���.���]�uW�W^Y��s�2����v;J��5������G�-o�Vp��g���Vf���
�*_,�r!�4?L��W@\�<��o
��9/4�)<pG�l���w~�Xp�4nvZ���'F�8L[�?��C��@��o�V����.���?���M���~���F��nYG?8~�'^�..M'#4<,S�����'�,�]y?����et�p�_s�a��)����b�$e�;����A4J5�������/��Q������������5I*L:���������d�~%	��}E���Y�������*�H	y�F��bI�K<�.��8�.�&��*S�����5�2��|�FX�������������V����X��R@ �\�k}��M&���Yw���"�RY9��`����H�("'�1~]�,Z���%�����TX���)���1Ab�����e��%�1�N��0UK���T�f�^�+
��M^J�V���O8��n��k��y��3������V��s���s%
��f������Wu��%R�m����T��M$H?���������������L#D���}�q)n����G�_D�A�EA����8�;=K�����uv���_�*`"����������1�`�����
j������0G*����r@�R'0\\���SvB!�|���H�J�.K�y��	
Q����U����si	s#3�p�ib�n��K�:���5���������_j"��.���m`Z��"�y[b���Kh����u&EEm�Ec��H��yu6��B��
�J��
��Wr�[%���X��rel8�T���������J��'v�~�J]K�����D0e�r�#�GE���v�i05X
g�bM��P����� ��JrS���y
7�4A��FU(C-�`��-W(�
��������~��m�k&���:����n���Y (g(��:��d�n���+D�k�%�dKZ%L��WP�M���m�d��=�qc���kazd.�2n����������u�]e��W���k��#,��
4o~}�
� ���i��jP�"&�Gc�lb-�aq[�ca�J*����Y�)��l0�v|L��]�sDkO����kG�R ���:	

�j�Q��J�j���[x#u�m�X�Q5�R���s�)�.�;4ug��/u�WLEh4
�z���:-�4��H ��������~����5��^��b[ghT�nb���^Y����:���w3����Cm+��Y&�������@��e�DG���-)�,=Q�Q��"f�S���"�_�3���X�e��u)�
�l�\�"�F9\8����pJ_�.\���x��!�PZ6;�����������Q�3o�QK4�S���D#65o�\�V�pk�4�t��`�e{UWnF#�R��h�5�8�,7�Cx���W{.��Q���9��E�������B0�4r�l���[�gH��Y�%�gQ���+$�o���^���h����wm���#/��U�Y�p|<�X��Uw_8 ^h�B���1�_g��d~�g���a�ok���J����W�X#��96��V/�V�Yi��;���[V�Y3Z�\�*���
$�)> ���� y.7�h[y�
�&����������b�1���%���L�Ya]����>r��]��z�:f�T_'��<*�(�����5�go=3��L-lmb�O_yQDDU�E_la	{qk�C���������y`=�T����5�RU�N.���
��R��x���x���q����m"��W$�����@(����%�fG�J������h�E���7��U�����U�M"'�;1�i��Ng�F���1���t�����`h.=���X�����_�:����c�mzB��
�� �����L:��� ��J(�)�/�@�SZ�m�������^��/��x����2�$I�Q0q<>��P�E�"�T���|����
h���.M�����#L�]B��"�3����+���EP��c��x������d@��AZ
!n�<Y�%���'
�+awK��^���Eri�_Y�J;nUhp�C������������e�/.�Y(���� 6,�����,��[&_si[T
��(TP9>�|�;AB�O��1���="{��a����
p��aL|����[����8�|sn"�5�W�����q����w5��z��h�l�����-�
����������%�`���tTv ��q�h. z�|\8�K��tb���e<��Fn�G3n���2��/%�1�*H4������(����Zv�r1�*��o#U���s����������h���'�Yof&q�n��cr�0��7��1�S
��������!sW�>�:��O`j�^���?�gL3�5����������Z���#A������H��:w����	Q���X�J�w+�"���VF����R((<��lV����fy�������|���VU}a��LL���S!EN��Vk}�1�@��/}���u��r�����/��:��a�	��k?��!���!-��;��r��k!����5�QE{�����/�����hp<sPhLz6������-���!�R�r3[��t�iH�-���8�v��[[v�����$������-������9H�_�r������YKn�@��2����(����R�@\_7��#+HZ�W��`wn!iA�:%a0EP��f���5Ea�a�:;�P/��b��6���&���T���bq�2_��Py�������o�Z.^�_����7F��fbBB�c�YP�0����ts���+0F����znP�bB#~#�EUQ��,���r�f���Q�_G=#�y�C�����CP�E�U�`P����}����� -
�����x�TP��=�����V��s�����`����R��l��q�jYI�xJy���S�C����������3����Y�%R���k�ZV�����������(�,�"�����?� �iv���;u(	��Jr���X�c#{�?��J��;D@�X#�]9��'/��#�a�)EA���^%N��
��x��;J���co��d���Q���
���H5��,���7�.a�Hs��8�@rZP��[ �u�c���5W1T�yU��DQ���bf`6�u�0�S� U�,����@�M��� �\�jI��u Tz����6dm�r��J��0���;~
j���&/<�`�i�B���_�Q5���\|@)�1�����
�x�\�_RjS�Qa��:�^�F� %����n6�F� 6���C�����^�2��x/'F��>�.����G^�V�K�'(�_��rx���h�9�E�6��9V}qSO�*|/s�<W�V ���;��cL2g�9H�Xs
<����KVT_�S4���U����h&�2G���g8*7Y;<��%t�����4-���mNa(b�B��C�xAn���A���������C�^&�p���f>�IAU��@/m���y��`��lx%���`b��`��ah���c��y�ZE,���&@:^]�X���T
��#z�Y���'��1��-QG}@�H�0�{C4Z��fg����cj����H���
Q*a^l���',9����E�>���
(
���Qsb�f�����	��!tb��0zu�,���C(?�- ��Swo�^:���l3���T�@����x�tut�}����-�"�(q�O�a�c�]G=�7������V�t|�d
X����C�
-Q�lz�+C�����%IX@'�����0Po]���\�RZ���d�����X�*�Pgo�";�������V+t��X��)�B���7]���Q�.<y�t����J���R�2�,���~jV����	pW%�/]��~��%s����R��(��R�0b��Kz�V��G�s���q1��!��x�X���g���$��F��aDs���" ��T��|��(
�l������Eqg���;�� �G���x���.$ms��y�W}h!���1�,}U�d���-|
�z���X�VZF^��D�[�p������B{=�y�0aW������Rf��-��sf�����}x�cX���������|��
G0�����J=�T��I�\�3���g�f�q�B4�>��;�����8
��T%J���x;o�f\�����d:�WJ�{����U�U����&0R���-�c"V
���^|��e4�8�XX�$?�b��o3��_��L�w�F+q�X�T�8���������G���w9%9�u�59�������*
�k�`A[�Z����dUm����.�h����;�m
�"�O�G��c{jI����st�i{��<��!��t����f��b�������zD���js��Y������.@n� ��\�r�a7Y��2����<v�u�d��\>��4QV�c:��!+���^x����:�k�[Ia��V�]!�0���,��Z��m�c���3$S�f��P`���3
����N�X�3���U�4��X���S@+�[�g@G_����!_L�6�{7�a����W�G�
{�c�k�!pPW���6 iFq�]��q*��Tk���I[a����:�W���#F�
�kx��-G�f�.1*��������X+������L�ro�"F��,������e���P��+���Sc���r���M�g��r<e�Q1l�n������q'����������v�W���/7��a��&%"�����$E����U��Dm\�Hi=��K�g���+���03�o�U#U�.���1>�5I�_$B6�K_+��jj�<�qn�Ui�����=�P���l��vYuq�n�CV��r���Z:�n����bQ#|T���[h�G�v�V�
��IElWIW��]��J��:Fu�w)H�y���a����|������ad�]����C`�'>�(����u��L�(=���_�/9n��&L�V�pw����B�Ma�^������~����k�Y���� [&kI~�IR�M���k����5Rs�H�i����h/�*�1����y)P��S���a�y����et������j8i����On���oa��#0���d[���_�2h�'6���"�QyO��zL�k
���Bduj�y�����@A�������[�]��a���hGZ��Y��
����(�d_>#Q{����%���@T�gm6��e�������<�G���
����[���,f�tZ��%2���u�K�B�0g��Gg��q��k�CO5�EU�'������d�b��m!=�zTPm�����h�K�����C��"7ui�����BM.`�8G���5R��R��H��������*��M�z�QV���'zPY�ti+7�kY�-:�<�7Xr�|z�w���KP�3K�g�["��CC���-��4���
U�w��[l_o�;8�n`MP�����1:������R
v��u3Z2�]����b;&GiWO�!,���^UaV�h���0���zt������q�\���8{��B�F����
<�=%7�ZW�����2�����)�S�V��r�G<��z ����F�R��������%q�`Y��#t��L��vq	Z���������O��*�3�O�c�������9�8�����QZ*V,`��A����L�L�f�Ug��Q�E�y�}k�G��z����t�����qE��j�=�J�#�8=�~��@a��*8V"�����+#r0o�!�����O���[7���@�)7d��x	�H)���3��e}o1%�Z���e���u���x�m�"��f<b �z�������J�`��14���%��
8
c�DkW�R�,�s��c���G7�����yr \zD+P��#�c�9H�c�;�^f2�H �1��+X_(j������+��g����_���q�=j���s�������4����i�'�Wf&|����\�I/��eZ�����
�lY�X������w��JV7�Z8�
d�Kz��/y�1���]����A���������[g�
:�E�����:�l�������F��a@�I���N�����z�����o�On�Na�-����	F����,�t^GoOF!-n������8��\A$�UZ�=u�����o�^����OJ�X�p���]��V���� :	@�}�������hiC�[U�2�|~���v����6
3���\z}��h~dEGtw������q���"�l@,q7LQ�L�0v���6a�����%_���A3U�x�����?D��pZ���|�_x��Hk�**����(8;��3�XS����&�b��ih
�c��B�g��t8:q����������=f,�^�.�F����!�,ak�Z��J�pr�z�)ER����l���}��K�7]��-�L-��u��bE��4�]�YZ9�N����%�s�;xR�z|T$R������*���L��u����8�K$/l
���Ig����Bj���#����@���Z� a����v@�`�5��fV�8@���AD���_�)=T@�z����^�����um�~���(����P����!�-���
��1������Y���s���N��!����p}�������O�Qmm_N���-��`#�YKg��g'(� ��S�e>�M:�b���*��3/��AE�_RN�Wm�������%Cv:/W,-�.�k�����*nS�6Q���!b��Mu��!J����u�L���5_7*r���|%�5�\�������cL���_�9��.�=�+R�P�F����R���i�t�$�
�g��H�e�����I�}L:�X�#�2x`�>e��\L��:��W`�����i{�-���>��A�K��(w��������;<%\R_L��t���.��)V����%��1���PQ����X�=���f�!Y��
��u��}/���	��f�i�@�9t�k^��p�Pc��:A�-��X�e5�7�s)}qJw�*��^5�a��`�L�=(�'��5fHD�h��G�Xl�l�����lsE��]�����~��"8>�*G5���G&������������:�zl����,\E���H����E����B,8�Y���N�=+�FF��C����Q:���~��GP��v�:�0E�1�L�@�����{D\�:�4oV���Ey�0��W��R��6Z��?I��Z��'M��isb\��}s�;W���'V�W=���N�g~M�F*���m;G@b:y�z�w�[�z=�s*�IW�V�b���`1`k�� 6�W���.
J�'7*\p{���Tgv�����2��W:%��<���s���#B&)��D>��h�.�m8H[=��<>;�0m�L�st}`
2�g������(��
>
�� ���W����*7�^p���9s��^��,c��� P��Z@�b�<?-����d�n��
���`iEK���������c�A@2�K
��� �#x��0�U��+^�#�*`n�~��eNNyu��sC=��-o��]jS�,���+D�J��WH���C7�K��2����hn�(~�������uY��U�0q��Z���=�L�7��R�6V��	�a�����K�j�(��W���/�u�����%j�Z%N)���}����>�9:���M�K*�
��5%�"N��������in�W�������}q�U@��{�J��V���>�� ������1�q�`�(m�{ �����U�����i�t:�t$,��%+7��r�?����D��2XI]`f�C+��o�����e��y*t
o�n�yvE�~�1��8|��CO-��?�	������@1��2���+�lv�$����p�9v:�$O��:�k�����K/Wq�h�O@;B��r=�f&����E���c �'�����k;�����9B`;�������z9"5*�X�"*����2e$��z�LU�Mk�����+/�2����������4���z�	 �Y��1s�EdzW{���@>�= ��b/�j�rB���I��d�-����
��������Z���	 $7;�x��T�����	J	����qVh��7r�F�m���?QW5��XA<9b:� mA����A�~�1p,`D�x�������������q�s����]2�nFI��]���n��w�gw�b#���E�X�E����K�>0���M�w���cmh����uV%����:�h�
�;�.�Mb=1�q7K���1%�K�/	;���)o��/��N|��pG�
,�
��s[�L���L{�u�#��[�����/4��W�b���a�W�
%e�Y�`-��W��zm�����Z�Sl���:����CM=���q3�UL�����x�U���.A|�e�����D��U-N��Fkn�a�o�K������\��{A!���u�vE�8�LK��h��y�g�\r��^s�0r�KW]����ANk�)��6���NK������,� 0J�t>G���
�������kn]+��7��zC�1uD��j&`������"&����N��yA`��o�=��g
@��YR�
K�y`U�3��=J�W�*��^�a�]�"QqZrq�I���!��B�U����.����=L�$��ygAN4�L�P���s��J2*�� �h��W�P4��e
/U�����U�o��������C]��czU�w��N�qSg[����]Xj�hv*�_��T����K����
]������QP}�G��,qt���n�-6�v�Zb�
�����l�u��E�g�Oh�.��/���-+�w�r;�,�6#U������Q�< ��f��OU�s\ue�_rf�Ae��kzn.!@��x�;G#���v5/K�xs0:J���4hS��0;Kq�:NWt[�	Q�_�aE�=�����qEV:�K
i��k�J�����1D�W1�� |9��Jq���Z���k�gO����������(r�5�UT��ij���=�uB���)��C��4B�^6=~����il}�������d��E�`�a�@0:�.d���\���1�����>��m��v��4��_��>�6.����[�� ��4
�M?�B��d;;w������)����P�8�5���a����s��m>�)���-=��;E�i�����$���zp����C���AX-�;�
�)%j���-�������5r��r���J�������/}�������_;�f�6�����T��n�j� N7�<�_�K�m����F��-�j6
��=:�,��i�>nO&���(r+>a"|[����q�^l����k����;�{�S����Z���r�!���nO!?��#z�Y�+x������K�]e9���)a���N�z�84�7o�cX�G��gUR����V?�>�`��j�4q�9�\F������p{\z��B�����^��Pc��s��8b����Is���Y���?���K�n�r6[���&T<����(�r������K*-��(�����0�����Ce_4���e@�M=K����K�=��\IN��N���^�k��u�5W��{��3�Mkp6�M��7f%x�'���B��N7�M��K������qp�vUf'2�������$����;hpm4[�->��
��B�>+��%� ��y hc���zB�����uf����L��6�s��H�j@�$�.w��������j�P.����e�l��@�c�������o��t(���/��p�!�;"�NbZ��K\������\��g
V�T%�d�]aj:�~A	���N����egp�^c��`�v���S����oZ��-z���w�N�_`if@8��ZTG+����X
r����c���7W+/{�7��d��u8^�+g���I�g,�Q#����;�����T5vpl,)H���1��6����9�5-���
�S��#����;��=_-pA=�M<�z�r��:9���;��(�����1������\N?^#X�zE
k�����V"i�2��T�r�2�Q.5���Z�5���B�+������*S�{�Ym!���
��\�E���U����
4�`���TL�65��""���z����p������B�A{�w���~���L]�U]f$(pLU�d=��OJ��\E�T b]5b����Y�t��q�����������[��b�Z���%�G��cm��W��}fi�Q��2��������t��-M��e#N��x�81*�����Q���.�O���?�&	�>Y��v�g��b&����u��s��6dw������kW�0�1�e99���xB��fu�`�-i���V��r����_�O����+�C.���`����/o�]6d����/y�O!������O$J&����78+��w��q1���H�q*�w+�m�R���gI�V\��;���������(�cT�/9�����~1����-��T��A[�zw���e
��
<�X�T�~����m%����������"
$������J�fl8j�� ��M��T@�����c��:�O��/A��%Kc���-���n�8bC�}e���?�����l0o�~,��(��iR���2�mC%Ix/1,r"+�-6���=����\S��u"\���`�EI�Z)ee�����o�Y�~.E�ONc�#�F8�h:���d�a8~���}iP��O����6v�|���������4����b���V'��r�+0�n��>/�_��w��$S1=����r��`��p�Z��X��Z`&hxY���Z����U���W���6��;%��i�5������)g�
����TD���),#unXc��C�_�V�������i[<A�u�j����$t�������~j������1�A\��_2��IbV��/� ���
�*��V�%c�~f���������=��>�
�����r��F������C\#���%#�0,z��J��X~>�boY��yz����w��Y��5X^��|{�P����VE��0��j�	{F���)E�b 5�cW��������$��L�-
�j���<i����O^�S�Ii�h��������*Y�6���TW���\P����^"��5���cHPlLuK��\����F*8�Q�������v|�8B�1��S��]�8u3_����{b:�m*b���0
C[e{g��W���'2���nP\P����#�v��KWWX�.��7
`z�d�s�g�.K3���W������K> �0l������3H�	���t6+�����iZu�)y����@��xE�0.6*��m�($��T�gRw�5��E����!��	x(��=L���&,8��J^�4L�}&+*�v�B�
����8;���V�1lR]�����A����6��xW�F�S,�Eb���
��Di�C�:�g��)�029*4���/�3+����i��d/$�	c��I�����G$qF����b�N��87H}�����x"f�J���!�(O2�(���0J���[���~��������MI��xi2�`.}\W�%v����1�����t���e�Hc����Z�w����0w,N�)�,���}���p'n�>_�8Gy5�P]��Fc�����+�����:��R>�A��3[�0m������I_�[���/�V�3���i����s0�'�Xs�*�X�+3v�:����*ha�C�R�4\d^M!B�w/[����4��Dm]\�D4����h�C�f��R��_2��i����� �Sx����7�~=�W�5[����\����^���}e�W>��ZC�h{��������?�yg�k4~k�V�])����������1]�,(�,fq�*Z*����):������u����b�^��*-j���|;��Qy�~��)OhT���g���?c4�����P!������w�������O�S�����J04umS�c�S���V�=�)��B���/}W��X6q���`�,����O�m=e�j%,�����*@w�	,>���)�'ExA�:vo�����K�*J�DTT��������4�����M�]e~4��)�.�R�������l<��e��]b��F-m����&4�tE��������QS��
���
�y����k����a�RF��b������R9�
k=�������Vk�("`�3��6��b�J���=Nb���+}�\�f��^��V���\�-AV�����"6#��B&�x�W��u>��+�F�o
��D����E��8-T���>�1�:	���l5Qe��ne�2g����K�����9�������7����`��tbIB�KM�fa�Ui*��_Exa��.)�����K�����/j��j}Lq#29G�{�M ������a@YVS~� d�i��_�o��������
�=����������?�������|����Sk{��D(����
��o�3��a~���/�� �����5�E���B����������d�;���c��c{~���q��?R��C��>� �/����wIC�������:��|��Jo�1l�
%W�U�������W��N0��3��A�;��iZ y-uld+I
%��e���U����C�������^0O��������B+[E�.]L��iJQ���D6�@�,8OZ�,�l�t��SL[{�f9�(�5�����{�F�K�\��hew\i������;�me������Nu��3��� S�E�XK���L>�����X��^��~�5xr����hjj�KsD�N��Z�	�N��9�"n����B��WTo!�Dmd���!��:TT.�W;{\��UWG��i���k�o�0)(�E����Y����_3��5%�>�@<CV��l�1��F�/C\�)x�a����Y��%E��E�k_��];�&}��ec6V)1�%����i%�j,�r�#�5�c4Z���Y�s6b�g��B�y�����Le0�W�Z8UF�>�

��
������yd�����_��z]4��X����o�4�6x={t�9+3�%uZR��ezn
!8o�����`� h/W�=zGYX]V��9@����
��zfbr)���{�����I�z}��G00{J��<J4�T,'�%t�rU�������t9"	��o���j���\=CT;y�t� 3���c�5b��ysMzEt;���f�3I��L���_���*������z�����
�,/P�R���c�"��)�WT�@�Ns�h^_����`A,����q:-������H	���]�b���9�@����?{���a$�b�6:\C�61����Z	x`�e���X��_���+V�q�y�SJ�z8@X�&����SRWYz������a��@�W�����_��[�[������o��T���_X�!��DbI1��W�{�^�w�Qh����
��c?0�!�R��7-��=B�3�V��FX�8A�`���C/��x�����R�`�6��d��^��"��]K��w�����8D�	���1P40^��?��4! "14#$35AB6FQa%2b���D��o:�wN��W]NV����n2R)B��2Q����L��@+z�uB7��#S�s����a������F)x�KhxY[v�	��%U��y��P>�:����d_�5�-�������]����t��U�^D_ULcw�V��Q~��t���W����To���^���vc���zyU[%]��V�������^����T;k�&���{"]���xl���x��B�����i��>1X����]����wK�J'��T)����Z6��N���B@�LW�=;�EZ/�^�g�[�Nn
m������s����nS�L��
��lV�1�`�x�vP��4r���������Y�������M�b���_"Kx�����X�R�@�q����5K�c���&��5����C���o�����jq88�M�}�'{R��"\�F<�En���M\�v7����k6����.�	<���H? zKO�U��NE�ir/��s
�4�F&K0~T����|����M,���[M[���^lx��Q��3�l�c��m�����#����d,�����Z~L	-A�
#v=4���w���\	*q����j�������L���;j%�]��zA��g/-"���K��Z!7Oq(�u7m� �D�JvyO�r��d��O�--;tj���+���f���$I{tDX_��{�p��6�$U�Z�3z[B#Z�@-��D�El���W�
=��cjp��W�O��*�\t�������>�D��UM�Z�n�{���������x1b���~��}" ����{��=?l1�U���lF^���!v���� �ha��w��d9�t`Em�2l��nw^Zm��
����DX����J�<���~@�*fQ�J���6��r��'��S��J&�S+V��S������xPIkE��X?�5��_y���	E�#��$1\�b�8����V�5o�*yg"��*�V1�^������EqAK��g��L�sHYM��&)Z,��o����-eD�L"�"JU�����I���K�����D����x�&�(�#��TE����V����V�<�>@�����B���!P����K�0�k	N�QOM8�m8�>��W�hAL��&�����:�I������+�%U�����5��$�CvF	��
�h�	{=o��*&K�-Gp[�d2�|�~@�}�nAm'�W���k��L�-{�W����W����w�>������+��6���p� �t�rI^�~X zIcz)Q��aB���4�����eypPK�jQ��*8��7,��x�;�%���cm�����h�,sCm6!#JHn�!zz�����F�U���9OjO�I�g��t��W�0R�
�
�K�X}Z.�s�CX�����
[,�R�*�
'�Z���k��A����]��mph�b��T�u&?~+��$��Ey���{���Q�C��M���}=�7$L������z/H? z<��w��U����"��+_�c)B���6����-*R%"�����RC{U�	Sj�k��q��J�f���G�+/������������*"�H���J��5��4��)��b�������g�$��P���(��\�UU�>D{
�_$�=.�+[H=U�\D����[Ur��fb"a���D��T������eK���tE'�J�E[�=�����j�!�����
� ���&0�Wm\i�l���Pq;nO�KcP�0����qu���\i���q�w�wl��	)�����;B���:7��\.��I�r��
������0��.sV����.l\�*V������������f-��`]�;l����oW7�!�A�YN�Rc���b�i
�G!uM��R���V�nH�x��s����jSS�/�����q�&?k�]���V����V���rl����qn�
`�J��
K]�������D*�?����<������i��.��>NR���pZt�\���2E�.P�V	q�rT�%b�nu�}E3���e�����5�M�����������/iq�wX2�z��������x�N+�\�G�77	�O����N����NDn=�x�}���g�o��K���niR��
~ |�Z��&��.��L�ne�Dk�[F%�C�����QI��F3J������yN��������n��y��:��M����'6�r�[��q���wiJ���RM�r(1l����O����$cQ�s����2cm<��C��;Q��7�>:������A�X���������Nv�U���j���*�_��s��(��5\q�l�l
O�EXRI��i�m���%�����Gv�O�Y3�u�]
D�M ����'��P���d%P�&��+SSZ��{~�Ge^�0���C�*�E��&Xt"��#�/�'M
L�c����^�����Y�T|^b�������	��eGGe�Rd\�q�_D���iQV���Z����t�t�E]5���v���9�.�����=�[W'\8�n�u�+U��Z5St����y�D�����s�	h�2q@����B�jH�mZ����`k�$�u�����%�^������!'��T���*Lr���G$AJz�f��_����� tV����>	!1AQ"a�02Rq��� Bb��#@���r�����`c���	?(��D�2���|
ic��@�E��7�?���������8<	�H��F��Vd�`_V��������	��	�h�[w��@�]"z��\�A8����B�#����e0�����3]�k]���qL&�{�FS�a\&�fr��TQEy1�~�ZZ.����a3�	�b��w@]�E���W`}NM����iF}aU��&5�j���.��O0����� ���{]�d�K����O8#��@/l/x8x���v����ZA�v������@�����M	$
2��y���������e���k�����`L~�I���o�������Z`�(��)�|��sNs��O2����;����O5�4)�$Q�H�E��O�i��F7@�!Z}��'>5�q�$��D	�u\$@�z����0�6���e��k��u�Nl�M�*)w#"��G��n�)��h�_�?�fG�G���Z����z�;�]�Lq�w�!��v�p2���>7��mL�^t�8��W��m|)�>=`�MBE�r��.�Z��z(��x���X���Q��.@����X��h�P��s1w�z��Wm��\���@�c���P_��������D=�����k�:`�De�3�ui�.����K0�`U������Y�8�V"{��aQX�;��U�����
7f���
�]���+ Ch7+0��Y��U��s�*�x�&��d�D��&�Uf	�����>�<�r���:C��'�:a�
�����`����������-B��1�F�Ag���UNO{�9{u'*�M���t�|(Xt9r
�X�y0Xj�7z�FU�?5hK@};�V��a�����7����l���2=U���Oy��B,�0G}�V��z$a4�%N�A8�~x+K��1�'�/mD��d�SvLz'�x$�s}eZ}��\������n���Y���x�8����5ih� ^���p�a|�����1��&p�4]�>
V��K������s3�oV��������I
�Ah&<����5�J��c�a���_�}J�,Z����X�2������E@19��*2((�.������c�Q�f���%G7y������|�F*��:)���R�������P�L�h��t�t��D��8�����|��(����S���.�O�D���39F�j��~��38���Mm����o��Mm������gh��7jfk��Lm�0#���1��������k��L�I	����5��`9��$�
���@��X����f�]5���p����"',�VL!������}��NI��(~)��\&`�8�rca�MI�8&m��a�wJ��G����T"����v��5v�x9|�ZA���^�<b�]�6f���G�`sDK]�a6�SM�#��N��x�i(K�2;�����R>k����4z�!�~O+�i�
e�v�� (L�v������3����.��{:+�`0�CZw�'_������;����qde��u��tLT|�.>��8�79�6�:�0��8r��.����W*�UX�z`�	��5�*z�����%� ����7�c�!Y:��#7L`���d�}n��w������VN�>b	4]1L{��o�g�U���H�#gEe}��v��/�<B�$��QEB�(��Q"�(����,!1 "23ACQB#a0bq����&���j����A��aS&S\�qr��\�)�����a��v��� �dJ���.����C.�X�cW-����1��$���;��S<�f�YS�S�"Jn�?�i��&��s0�
�P�IM2���}gFT�Ks��Bi���~e5�
��;AE~D��v)�q�M����9��j��#�T�t.&�,)� ��1*���.��U�]*�Q9S8w��QT���^�s0��
�4�=eB��
� �.c�K~�������SH�O�M�������$�)��#����g�=e[,S&�:u�}gN��? �[�T��f�8�	C0�ng��D��Q�)���}	������*�d
���x�Y���i��������_��]9U*����^��[.�}�\���^��];���w9�N'�t�����
���7��0������-��:x�QR�f�h����Y�c�k*^�T���:������8V����B{M��nl�~aD�O��:��I����*Tu9��
F�L)��z�W�:p�AL������.t*H�3�(�`+��#�t�-!���d��=��RZ�6m_�*� ��U0�^%5�=EB���Ye3D��/�*�H8\�D��-�mC�k�%�[0h����x�QR�z��u�H�R�C�l�# �C)����4����a~�`�.�mQ���`�N�O��T�e5�
�`��J����k��
�L�'4_Gwe�� �#�t����0��N@�t:�S�F�sqQ���U����gJ���#=����C�l��=r�k:�ic�;�:�[�1�f:%W��)���p'���gsRv!�p�L'���
��'7*gjM�j�4H�.d����8a4���ppnkM�Wn��L�1W��0��8��k�n���nfA�'6�
�?�z+���k�E]k�hpp�B-�sC��&�\�r�k^���y"!����%�['#�X^a��S����-%]����:�uF���3���=�2���P�"��Q8���	�s��*��c�l��f|��f#����L���K���6"�sQ��r	"N�l�tA#�+����S9��@��sA�6-!4��i���=p�m�'��@}�Z�{O.��`8,���h��������C@~
���;E�{���?�64��U�F�?�:~���!N�	������!1��{��n�6~?��{�lP�+h)���'� �\E��]Y���
�p���������HsL��PL"$`Ism��������SA@8i����c�L*WA�x�Z��T�F���<.x\����Q�D�x_��<&WO�pM��N�$�+�!s���V�,*����! 01@Pa!AQp��	?""""���n����.�����^�{Y��8fg���<=��l��������?]�7O�w8��O�������8�S��t8fgl�q}DDDF���#A2h�#����(!1AQaq�� ���0������?�N(%���NhN�H
����%C�|��_]��e <���zx$��I#�t\m���k�� ��}��;������C ���%F��%�)�a5�������E����&v|I��uC��	^�8�Q�;�'
�
��U�<���2��#$��R?xt�/$]N8Q<�Um>���qv��l��f>��AYhS�^i�{N��t"�[��9�6K�8��X����fv2b"��-�R�
�KD�Q��&l]�I$\�&B���P����z�0EV�	� ��=' :^�eJ���v��,�A:?���Q��f)��	��	��B�����;��b�����pf*��������H�
Lc[�i!,P$KD����H�b�@�J�`��//���v������g��Bh339S�1��#z��b���8JG�c�B��g�a>��0,��Y& ��}��������=��p������I�
���xB$��c0����M���(G �4�C��4�GN$b,���l�c5����}�c~�ts<�~�������IH�C��"�MAg%��&�(���/�#�:�]��`���qr@I��@ ���`�
4����� ?��P�fL8��z�<�9g�X�gd ��<���B����|��`�t� �"�0�h
U���D�|H,��n��3��-�h�`�\%���P�\l~?d�	���~���JID��1m$�����A�$����vH���@A�9N�	@%C�d�:�Ta�Oc�����Ao�F��"��(�7 ���V8@(wn�"E�|w��s�1,B5S��#&��������e���/����
I����$���2:D'�9�?�-���d~41�l������J����9���S�f�����X��@����HHS��F@���)���B
���uz9��,S,)��
`@�^�%���]0�I����#�e
�X�
��� �|�9���*�E�#��h�%�8��3�0IxP��s�B���zP�p$�o9�����e��$�2��$9<O, C1��9k ��wC5����| �k���P���������y�TA!I���P�2~�����!&Z�Dv�B��\��]�I$�<��r@���	���)1Srd�FSy!��Afjc�b�k�$B5�'��HCL���w��]Q�r��d��-�����c�1,x�@
sl���Jo�c��&cDLb�PXG�����3���;XB�Lj��0����
�q8��r�J.A�^/�B�l��g�]��<�������x!Ig��?�^V��o�oq�����H��;UC���qiI���90��'hfd�^�����@�(��F�K��u�53�M0���?������<M�~��B��b�-P�:���F�b��1�(70Q<s�)��Q�P�BYH��Tcf�{�+���@���3�*n����[� *d��'�;P���\����}�;�g�4�r�L�q�W��,���.[���4A�"�����C������I�mz��P�bPfnB�N'~L��2IDm,@I�(�g;��F�BYt�����s��5z_���|��W�o���BK-�KC����~�;8K[�BQ*��K�P�{������
:Y
�YC�NJ4^��wN]f��JA��%T��Y>���	�6���,�Ia�"�����r~���)d(P	T��nd�c�	"0IC}�-,�3��D�x�N����=#���$p���q��3�����������*z���XY����k=�o�$2"�G�`<[.X���X6��p"�l���8��
L�G�j��A0�h'x���n���a3DW��D�	��F	@BP0��+�8�f���"��=23`*��}N�dejw�0�l��fptY	$I��L�g��d#4`s�pNb����!`/������g�~0���H��6q'yJJ��HO��;���g�xZy���������@�c��'(>\�J�#�QyX�%,A�����q7�'��1!O��{��$�!C"��a�PxD~�r(��.!�&�g�$�>���1����6�����)�I
3�H��LH��(D��4��`��/��%S�'g�6'�O��#	���\��������]�|�Xv AI3y��R�+<�����b
�|�E�of

�'ymb�4Q1yi'�(%U�	U�j���qT_GC�jc�$B�909�He���od�%�V'�����8��I
C�5j��� ��'1�VX�"b��Q��'�Ji��4!�?{��V���x�"�6�~�0��4��$�V��_�;���#������0�s������	U��^��8H���0�1�U�j�abG��IjX-UZ���'}Da��B�V���	��������'sh�#P�z��7��	��I��� <����?��� ��"===�2*����|2:)��E�<9�{dF@fY�,>��`x�����"P�A�p��������_x��L��O��]�@��&���JX��~�gi�b&8�7�!	W�$�,�@lN$i��'^�h�^��u�������)09
���QH�c�0�x��"*bl�braA��I�%A4fdo����M���z2�b�+W��^�5�&�P�jy��Q�T�w'��0��L[8���=���,�eVC��T�b �����Iv�����-�2����V���l�m�������%Z��h�S(�D�|$�k�0��1!'y��8�� �>��$"uXy���i�r!P�(M��+%��b���N'b�����&�2Ehl���p�J���I��)��s&���2A1�e�EQ���4k��n�8�����D��x����2�.��X�=d�Be"(-��AD��I�
)"8�Qv�O��q;p{��a7��)�[G��(F

�(���|G���`�2�gC�i���q1{�!G���+��+zQ�T@�"�K������vU��{��BL=&��*�U��[�k���o�?�T8m~����6�6f�dP�X�;#�yM1q��I+3:������&Jj�>[�0��fIK�R�@
�"P��-�b@�j)� �2���Fx��1�����Jk�Q���7�t]���
�"��	<�B�X������4�T���fQ&�!-�x�&$�����Vl	��	@�0�L$U��]o;���P��~������2,����8�~��8�or����(4�Q<�	����p# E�F�t��>i�06��
q���rFT��}��dc�%I��
t�� �42��;�����73�w	^ ,"�jE����Akk��iaJ(W[���N��N�3Yu�����(�P����,bX��Ncy
���bQb�E�����,�A!1q����"Aic����%\8jQ�x-�y�#*�R�.4��;�fB^�r}u2�_n(����s��(�oV
;�"�|�������T�\apU0���DE5�)`��������#��,��^3��MBz���N�5.H=���f�N�� FM�d�g�u������1�)����DY	����G�&�uabP�7�5���J��9�Dxqn���O���}>�8�`�a��(��d��K�1���1P'�a��I��*W���UE��t&b���@�H(lzF}����$G���3��A+	r��hH�@8�v��"�.#�t&!��1�A�Z���Ok?b�*�BPQH��&�1BM�z����*��k����G ��cW�d`#��B2�M��p����M@7F�����`Ds{�Xv�2�s���wO���@w����@�8����A���%N��4�\����=Tb���+`�	������)�Mg@�6$���~�` ����2@4\�9�,i�P�R���H���BAnL�s�*5�����K��8��w1��J������F���&RY�<F��<3�z�"1�
}	e��%��?�y�FI�fH��6FX���~��i��
~������C�Q>Q�9�9T%�]s��nq�KK�w��<HtGu��")DM���B�����2D�T�3���+Q��(
�F`z4&O��P�0
$g5&S��SUx��'�����1���"CkW�#�k5=~C�$��b{v'?8:��'J��D �����f '�v@NRRj1�0I4��m4h�v.@�L����oa0�	��8��t�"3�&Ml��Xb8Nx����LF��?f~D�/��yd�M[Z�@���S�a/;��4�U�c�5S����:rU���OR����?�~��!�L�u!CE�Lk�E��Xq`9�#�V������\�DZD�`��
��4��v�h���P�$��������1��A�����&�'�l �������F��g9PRSz�9�y-�=��ayHh��K����3p��@��?��H�g	D��!2ph�P���@�I�����`A��x��]�D&�_`�w�r��s��i�7s��(%��y!�3�>�� 9�0>����8!2��4'F���h	AEB��X�+\z��&�NT�c�2���9w�?"d�o6�<O����@F�g�}|�&G�i-jy�b\��������X��mi��
h��#��(��a8F9�qh%n�lGa�j���)	�p�������0����=n0#�I"�pZ��y,sBu��B�j����@��6(�R#��g(�'�C�>"FG���	QE��0��_�=��"���<�[�*��Gf��dSS5�s�LRrX�5[T<7X��hcb�)���%Tl���L�'�9Qg�BH������`�������LQ�Y��$��{(Q#���`�t)�|{��2�@:)��������wN�Rx�@)/�vhA���t'�*mC��R�
���@Qi9O���%��q����vi�HlX��x�9�!B��L��@oT�A(
`}X|@
�Q���B��c���e�����j�t$��y���.
=t%q����?� #���z���1P��X�����a����V#dPZ���E��gN;Ra�b�`h�5��1��%E]�Ezbt��fx!�9�����`@W�b�b���@��������$Kd?��b4�
N�1-����*��e_� ���������W�	d"��(���N{l�8��
�L�M�(��(Bh��]�@��58:PxD'�H�F(��1
p]�m��e'%�S �!&i�7`4~����<@��a`�P1��b1S��7V�p���9lN
/�r.����������N�h���DA{��A�[P���6���y���er#1"��$
%P�U�X�/5S�.p
��X��3�Tz������R$��
�&K�=�����8.7���`L��,	�t���>JBo�����D}gr����y`z�E��4	@��������~����>TI���?�h5y�)�|x���F,H���,,��k�c3 m�sB�qe�9~Dj\�2�8�����:�\Q�&�0�N_@2�qc���pf:c
��E��IK?`���U�qz�����!����#|��(��/k���M������S7��3���a��	9�%@
�#!$�#�Y��H N�& 
��plXb���@��|����Rx��]+<�F�nk4'����;\V!����F_���5,�q��B�������9�K	R��hFYc,MB����@e�f�����'�p!p�hA���}?Q����z%<�3Bq]X��46Y `���9��l��m�$M����5�j
�]U����q���D�b��u���(J��T��? H
�`�\��o:�P�X�bQ^Ndic����X�����3qv�����#`c��R(#W�FX��2���w���}��3�����	h�J� 
\N!�h��,&��F��
PP��� M��6X3���#�Jb���>�H���L�J��b�%75/����J�h�2�j6�X�� %��}�D�ji���
Qz�����f��R2�;f��(��~��(�B�� �����C}2gP�9^Zf�q&��e����@0h	rY,�� ;�-�A9�%�����@�����x�FK�;���)�@��)������'�c<	��#
�XC���e*���u	&ks�JI���A	$��E��F,%/O�L��t3�|���)���}���?�EA������})b0[4�q�*
�J`��A�h�he��@�-�_�3��h����P� Q���0)��W6&NAQ|�:�4C���b����I���E*Fz��9�N�RV�rD1N�
�i�B5&?`_�B;�"�=J�	��luC6'Vbh�$��FO<2����B��E1��H�9$95SX�N����!&������tc@"�%��)cbPj���22��a9�3B(����k'�5��)���:vJ��`����	�i���v�e��&�����A	]#�(e�4a^���	��;z���{z��bj��(�G:8�o`�y��U�#%f��[a��"	���c�b���	/G��P$,A�$cbY�`���a4��@j�Nb�z��K�;�����hc L�:���y�)��SL�LErw�c�!�#@�?�{s�6#?�8�������1
�T��/������"y�VP���BVy`�������bYx���H@w(��5;=���,�����F���S��D�����d�o]K��	���`,	
�+�:��4��BViQ��:�8�`'PRvx�'���� �l�%��1��5\��>�)Y�=OG���$8�l�c$�Bs*�=B�����&����6)B�f=�� pH�I8��mL��� L0N��Q&�e�]z=�~@A&9�-����8�l$?����O��X<rVcu�@�	-���n����!)MmB;a�A��pD��f�jZg�h�N8ab��6K�Q��@BA�����	�j�=�$��o���$���o���I�H�@�
O�4lZ!9���'�`MRL�Cv#=)�������������l���}��f��,�w9hF0A.���#"�L�@��~�W��H��	����H`�#�?��:�5�"{��bb��\�"R��#2V�Bt,|4������b!.�j:��&8�  �W�E����(�;?�|�����������L@�"�{,��X�Jb\�"�<!H|�5,�*(��I$����d��b�N�}	��+��(o6��
�+�dP�A��C�LB5�%��1ewU������������nDSos���cz�&�!��+,5w�Xr����'' a�O�,yDZ2��r
[Z4���Oz|c���v�����3������t4?d9�=��3:�y�(A����Hhp{�������4���=�Y�����.�U��6%��(�0>�>��!�p�����
7�#����2�Q�@���c@X�U�^��3\(�����M��$Wp#)`o�����>��=�����NC�HBB��QgU������	�{��y`�D�(B��QI�#�K���LT&A�JJ���K�1a�Xd��%�q��Y!
�8PXU�n��F&�����B:�>U�B`P��Y�	#�\�����������<.UnY�8n3�x�Kj��!�B�b�FVA�A+��#�^\tk
|�Z�����^���������`T��|@����?�~�;��2E%�g1fl�`�w1p����
�Dq'��n���Ds�,H
N���8�l����(#��)y�'F%�8)�q�)Af,����
��6�Z8��7�%+��H�t!���cBO�K�%�vW*k|�@�B��7j/����#��	���b��k�E4[-�����P$OP���NDABN��)�':�V�E�=i#z|��"P�P�?��`G�{���0s�$&z���w���42
^Z���Fa�	i�jVK���pWkF���} �5NF���	
n��������?��C!��_��0F
Q��(�a�"7�^!4b@�M��"9a�(Jt
�����w���Q}F�������:������0� E�2����F7�����;FX�I�0Dp:�/��WDV ��-�H*����'J��S3dc/�Xgpf�(�l�x�8��3�00�*��OI��'$�<�3��X����X��#�2�cJ���Y�5�c��

)����R9`"%7g�_�3�@���$��.H��6��,�J=������~.�1,��K�[39�0�r���R�u��v�J��3`@�7�<�D��H����3������?�@���.����:��j%$���2��t�X0��'h�r��
��w��2��b<��m0fy��!����nU��
Dt�v#�=O��$%����<�������u%o�*��J�!���W{a�4�-�p%�����Pk$�����IG/(���,�# ���Y,�!�!F�8h�B��]p�Y�z�1RQ��23���!����;��
;��0����>
d�����2|��vQm���A�4P������h�Q��,�!bK�e�B8�~!��J��~AG�|�o4^&{�9�`l��:T�?���J)����Y<g9K:S���VX�Zof:�-����!�X5\t����M���0������r�"�E��FW�q�fYr4�&I��������P!�.v�3�	��a���< ZX�g/`������'][(q@1��f��Q�	��q~�&�@#�y�Dbj�L����	%��"A$�������g"���0Ef���@|L�C�I�&
��p�$>�����rq%�-�g�:��S���e�c������PX��XL����9P3deiwRE�A#��r��d��C-�A3d��2h{3<�� ��<�0L�1"y�+$�|8@(|�� W�p1���M%�� b,L��ACN�e���k�r%�l7bvc����-�=gV���h36�eH@<��"ZZ��\�� NlDM������T��'S����"Z������'Iu T�p��IQ�lng��M,LX!
}�y�`3�	ic&p#��Yb���\��$N9�H[���3��G�@o��I�ZO���,�=��8����Cy!
R}e	�]�bd=�B�H9�5{�W`���Z9�GD�����',��Z�3X�	4�����Y?X��%A�x����x=�RF�E�����DiNt "�S��'��yZ2���H%~�	�'����`�DR@`�MZ$�oL���aB*����"�+5q�l	d���4'�
�j�"	�@���� LW�
"s<JIJ�a���G�'��	|���A(�E��<Y`q��++� ��{i(�h?I�?�P�?�2��� �s��a��5'�	�����=������i/�9F�l����	$�qpgBZ�s��}@u���e�i8�bUw��@��hL
�op&Q�@���J�Z��")�7�l�?bj	s�����`&�<�2��I
|g�q�^���������^�����H�a���X$Y���� KDx��Pa3�B�h�"����	DN��B��ln�0l&+Sr?NP7�J0p	��?laR&[Z�1���/��@������?Y 	������bxR�e7�+5�UW��`�&Og��BB��, ��=	��5/
����r\�=�k�`
���l�	���zi��j��R&Ic��8!G><v,��R���

;���'CN�V�CPpR���I]�hp��\s�������p�h�������&B�����5`i=L� u4D�e0��)u9v���d�n@�m�$)	7�@	���5���L$p���0q�b�n'������:�?�]�)F���>�� *���������O��`!���Yl������6x�^t T��Q��(T����< I��X�K~^�� gb*�[y�a�B)�&d^k��K��+9H��	�L��@��3�"���"��Ls��P�ZX��I T�x����M�c�y�`��*O�!��8�4&��3��#�`��1�",���<���Z\��&:cx�#P������J��R��E2BQM�B�^^^/��,��X�-���Q'L��'��D�Hk�-��(
|?��@��i&�g�(&bg�E������8U����V�`h�
Z����2?�g�_��Tn)<�0C�|`�kp0��ZtV��.�d��t�%L��0��O�$�����<�H�
�i&�(`�8�p�Mg��,�Z�
�p�\���ay_Y�J�Q�Fy\�3�4��k,	�tF���WK���)�u�`b��������Y�8AX���8Sd�����-6)p�%(�v.7P��l8�MEB���@\\�M|,C��"�<96�D�O
bZX��F��(8@T><m�SE��'
�%��x�CZ_a_Rr}<��g����y����B��y?�A�|��?��B�,c�b�����Y�I#���$K"�3����9'l��jFM-�7�SO�4���D�;�h��e88F��#�l ���
Z�x��,	�S��7P��� 	�x3`P�x3����|9�KH��Es1H��4^��J���������39a�z9\2D�f�����G����O�;��S�	������	�%�z3BhN6�������7��f���"�Y�1�B�$s\p���e8�
o��d���\O�u��I�=�7�H�X���z�6�g������%7�!%�`��r�sy��5�(�Llp�c� T��$���v�ZW��t��i�aS��Mv���-^w*����K��8X����Z�x����y������f
� �>1�
�E�������Ph(��?qL�s�4U�����`rI��@I;�4$Yx�����
:�4%"E"��6&y�y��iZ��<�Q�Dpxp$��k���5U�3��?�2B����.w�}��c�*��N[�AWS�����a3�uAy��/��w�=.������Q,zV�_9^�bX����9�L�$�@B~D���X��r4��=^26P����!i�������%����c����F��:8;V�����f
?�^���y�Fv�����P�����@QS�B��*j��h�U�u������
c�y�1�o���ih�41�����>���v�X������/K�46�\b�U96*�zA�v�����h���o��AT�"#nv����K���)X�0J",A�����0eHYt&J�|VT����W���t)!GLL4
��

4o!���C�)�%��`]��	���%��*�yw1X,�B���!�;��AL
@���DnGxzE��x�9�#��!a���q��J��tV�����z��L��'�v��E�B��S� ��	mI�EQ7����e"*$%.}�@DU�a���gL�f�8:�D�	�,7l�@15�����A��-p�����\j����e7�$@&����}�A��QN�1-@���
������\u�%=R����d��h�0Jhy��������%��I�K������>����L�����:%�5�l�!���5u	?l���*��v�fq	G���� Dc�8��� 
�A8H��G	�"�Oo��&��{
�C@}��_�xK�T�s�KO��1OM)($���!���  ����e��	g��^}��2$*<���&�E��:'JR����������8
�/<��y9Y���VJH���"K�K���7J�*<)���jR`(wc��)/��L�E�����%�J�7<��<������]�[���	��YdA�����+�a���P3%<�#�`6�bY��"��-�R�J9I2 �DkF�&�<��t��bd�p�R��g�Yhd���� JH���c�9��`Rl�?����G�1J-��]Z��$�9�&]��@4��@��B�[����-%�M�_��9��`" ���kay���I1;�=�T^}�d������P+:�\�T�j'(���G��.�D�����:��z�(
�e+P�I������l�d<�6?�e^F�%>?����j�$*`��B6~<i�M&��F1��32���A";�2mn�8�CT@�fS1����s�zJ-��n�$�RF�[1JN�DO
�Aq�)J�N�4��d+r��v�-~��m��������<�4��x�!B��P����';N)~2h7f�Y���P�?�{���������P4M�������9�
t��Q��]�l�d����D��19�b�

F�D��_q���*<%F�6|N" �">&xaEQ:��`KQ�Ma)�(�'w�e�',�i��4w���}c��<�-�	�H�Jy�N^���!(r����(�f)bt�
�GS
*ho}8�PQ6�6r	D�	c�a��"�@y��=���0����������ZL>���/9�0��4x����pZ~��C��+� D���S�1vI�*����-Oh�@~�|���#���%��?"Y����"3������:��3@�>@��X�DL!XjKpQ�y�G�����8��&�;��(���H,�����	���T���H�P6C0�g�0�i�a�F�`�Y�k21YX @���r J�I#<0 ��`d<�b�G�^]��V�t���|_.E
��tk!h�$F��C3
��(����)C�/�����k|b4����l�	%�H�#H�"��IH�%���=a�I7�s�7�%��2�I"$��B��Uq!GZ��!��J\��S�%B26��CW��W��@Z�(���XG�g�V�"!U��PH������R������d[d4K����"
�*$2���K6�b�����0~���[��P�~l�������	�X�2���B8�ZDzn�4�I���E��H�u�9C�S.WxwZ8����P(��>'PB
 ��pm2��	C�kNjsb ��GN'W��H���b���cF�T���L���s��p@���0$��$#1Y[�r�����l�F������G�" :��@�r�����H�a��:a`+�L�	��5M�H�����nz��J-�:������o1�l�t_0a+6�:|��c�f�b����'
���D�I��Y�a�H�f�X��1ND�/�c����tMS9!�.�H�,H
�>)�����mB��Lr/	&K��E �D8}?�
�6�ZLT�R]�<�$�78k�J���1�`E�v�X8m���!G(a������c/���0�A��4����_��)3�D�$Q���q�8�xvVM/�SdA�=e_
����IQ�z�^S�R+ ��4�rLe,�d��(�*�j� HM�m��.�x 2m�L�f�`
Cg��DB`�t48|��$;Aryb�X�0�[I	���@�}��:��PQ��Hs�,�`	��o��''T�D���y�o�
�1D!$�%����0��2i�D'HR�����&3@��p�!&�R��2u�$�>C�P�w�b�S~�/���@ax}m�p�l��*���j��`�2�MK�MUDW ����b-�!�\�I8�Dv9h����af���D?H`�C�1������"#�.8> ��Bf�Z���	��������t�X�0an�NE�c
��Hpz���g�yD�u����r^Vma|/��x�u�<�]E���#�FU����
�E�`�EX��nw�7
��l,��^���B8�X��� �l�A�5�bf#�%�}�������d��L��8j�*A����$���	� T#�R�
��=�F��T*�A����.��;P��I���B��Rb�u�5����2`_�>RwQ2�o�OYV������5n"
�8������N���S4R����LX!
xb`�C<*��0�V.���z1Q@����b�����7� q��	�8"��?����B����
|I�L�*Y�	��������7�*,xh`Q�4�������o�
L�R�oe�th��������/�L��y�@��Y9@��(�Qj!J�=RD)��.�a@���\���@���)������JK	%K$�`�T��ck =u}\EL�����$;1<�w ��J�%Y\�C,�������v/�V@"�Q5C4��2j9������&l�F
L�P�/{d���=��&�h��w��b:�hD�0���Z9W��y��f6��H���)� W���(������I�p�����~�H)����	�,�+81~=,���N�$�,2Dh4���bZ�@�	X�?����V~Da�H�S�q"`c�	�z�Z���Ty��� ��(I0��&�F�3�|������U�������&$�A�I�%��'���P\h'V�D��%�$$ ��X E)�*�$�~j��<,tkL�Y��D@>�ke=JH��A
q��c�<�6���E�$�!&�� 6��r:�(	��
��"M?����u '���Sp��l	Ruf �������r	���C�1�x���L��PC,���"������o��pz������)^b����h�s���j�#o��qMJ)��7Yd�q�}�>����
�/F$�y�=,����*�{ �Y�`��B��*���pD�U��B@�6)���J u�*�D�aC	6Be0c��q`TC�S�'��\OR���A�(��2�h�*��DB-�&\���%@�iz����������V���(��&����	�T�a���y{C�����]a1c@L�X�$M,�	��������#��B&:M��%��Y3@�R�RS�����$C�)�����J�,\��"�9#xWAb��sy�OS�&v}�l�f��5��hLAD3���t(�gE�e����Y8�qp��k�������T@�pcRS�����8�Aw�����C\b�!4�&K�UH�]9���3����uF��.������Z�����H��g�� �9
M,���`�jH��)���I�����b@2A	{+�B!$�	��@c� lr��m�B{�G����Z�k��Aj�c:$(M4��uF���,��>�E�v#	����9.zvV������y�F!��jD]#����R�0����n<b!�
 ��}�;X{��C#������&�N��	��%=)�Ez�9��"�S������I��<��2�7����8��V����^g�DJe�f��<�}3GF\�8
Z�Y2:]���"����,�ke��
�"x�+y@Qs���|�N�\g"x�Ki���zYbH��Y�M��
��w��\<��|dR�����r+��BKXr��\@LA2�����������e����-k�)�1�nR��q�[2V�����P��1D�a_"��W���S�G1o�<B#AH�C'��N�-��6+0�n�d�y�%Ing�Z�.	"��z�����Z6eV�I�pG�����P����P��	��XG��E���2��Y��j�31�	�t_��:��rOl����F�^	��C���p`�f]Q`���$���%WWFS4���T���*��)��E��I�������6	�2���������ZKPb��2IL�ZA�qa�����QKNG�o��I</ �Nb|���A,���8�h����1	�>�-\c��fi "��*�`cJ����2�	���	8���[|������'!1AQaq����� ������?���7�+��������my�W��"��)�r��by��X�U�V��aI���%g�.����T�1��������f&g"��:��������]�JA!��]�������^e�:�4�j��b���y��q-6�Uw������-.0
*���\1T������:����6���m�]��i{jw�,��X?�[���c���>XX��&Q^�C���u?���7�~3���������{�&�����5u-��������%sA���{����q�����5������k�PS�~��V!;	t)L�Y����n_���L���&�(e0�X����D� �j��
�u�l0�0��@����H������]�`�O|��=��E����
�Nn��D!���6=��5�����!a�dt�7&�4�=\��M���O�*���s9��L�P��A��E1�O���.�-V�f�EN�
�_�Xt��]_+��%�E����.��� v%�������O��O4����#��+��#�����A)�����M�2W<���C��l��x�1�Atv��{��aI�^��R�`�q����Y�?�F��U�������%(-V>�:|�KJQ���g
��=J���.��7�����G��W7#�rp����d��Y��0�����_��)5K�
)5���H��J��:�4r�;���K+����'%�
��6b�����.���k2=Y��n����������?H��[�G�g�'�g��Q�XJ%YI�o�{��W�x�@U��"�g�X*���;��x�J�Dd3emTV��-R�cI�(�3Q^�=�w2�m��w�.*��k:���n2<S~�K1��>���4���Yf���{nW��lt^�n��MW�W�J�!�YnM�X��f,�������EY���4���:2���o�	O
���w��}�'tB�@�]�z��
���HQ���t(��Y��3�_o�Ue���G�	Z��Z%�����1,�R�j��73�%_���Tf\z~O��/�U�E
��[����c`�X�����:V�����P�lo�f�z��Dn��~i|����4`0@��#Z������c��J���Z����V�
u�H�3Z7����c��R�7Fb$4�{��uH�5����mko�o�D��n��^�o2�@LL���{���Z��=�Q�L]_L�g�SCF�\Eo����_��]�+Du���\�k�D�9�Xh0�8�o �c��!��w�4���%1����r��w�-4�=`I]��2��|\GHa�28��F�Wx�q�S=O�q��he���O1����.-�������/�T��g��{MI��4QdI�@�2�Gk.�@��fU.����(:^H�&��b��;����o��?��gF�����]c�+��e�']�\a�sZW�3`����7������2��������
����j�CA�s	j1�
\Wf`ja��~�)m��C�����g�+}O��e"n��6 ���:����f��
��h5�e$7o$�Z������`�$�-��s�j��6�}>���5����"��O��C���h��~e2���"�q�.�U'�A��"�F��7L���w1�{��
�YJu��eG�-��}����L�4T#�1�����RrP+��w���R�8������f��_XW��,���a�����*`1��
wc�>��>��}>d�7�� �")	��F6����${�e6�K��}�OLZ�y�Jv���4�o%�����p
aXz;������h71��Y�"�Y��gX�h��&��Q/�NQgl�8pC����IQVq����j�`5�9�a�z�J���X=��e�ng��F����%v�lo�c���0U!�[�nC�K K
���>k#5����t�MB���]h#���3E)<�>����SV�'���
���c�F�
�*�0�B�`���!���`�EB��8�B�P�l���S,����	�+�;o�.}���]���W�^}��:��'J�G����jZ9?SK�����PB�8��f6�{=�,Ee,K��������
�O>"wn�N1&{J��~�o��4�7�W����~;@�W��Y������a`t5g�2��T!���Sf�.��T�.P.7��M��y�2���5|��B����=���,m�2���k�o�,��4QJ�[m�����Q����nr�	Y6&lAT8]up��J0�V�4-f�c
�V�:�g�[�
:HZ4�U^�X�\J��&�P��:B�������*6�
?�Mu�2�`�����1u0d��8��Y
�.����ic��(
#C�^(��	A�����q|"�	�,�<�r[�*��SG�m��������H�HU
[�S���d;��_��3�?f[�]Zpvjf����p/W�����\(��r���,K( �Twwx���Q�l�y��[��&F��=�:a���9gKR���W�|�:��)�u�:�]el
������%��u��*�b�����j).A 9��j ������M��������r��qV��c��gG���U�w��x�������^R�9����o���
9���w�����
�V82���r�CD�5k�K����
���1j�_v���B
]��iJ��T�����euU�P�`!0u�Y���#�mG%�S]��p+HU X��H���6Z8���;�0[*M�/��D��@�(V��4�
�[0�����PWZT��r0��i,���[ $�oFhg�"�)9xW��[�M0��0e��b�w�@'�*}��/5�{��f����}�p]��(�_�x��{o������������yV.���}�/,�J%�� �l�Y��,1����0J�H������Dvv��+�g���K����{|!�B;���&�,|
��<�8���k���7�jD�v�P�����]>c�D�Wd5�Y��3q������t�xt4d���rs-�
�E�*�m�Sz����X�#��-�`�S�J��-�Y������B���P���2#��Dw��Q���h�fn@4�W5���f��(��Z����
�4���[D;�,l�0�?�6N�J#n)�J��Tj�����I��c�R�Xqd�t4�����Q��e��P��&J
����Zb�,�b����Y����Y���yS�_<�V2�:gy�uPk��
K,~�X}��_����k��~�-E�k������ ��� On#�
�Pg��;!�]���K�zD`~�c�!��I�?��'!1AQa�q������ ����?xYN��N�tR�o��T�c��������1�������}�����R �%je���F,c����MT4PO�[f��o�]\�o)�z��'E��t#���p������v��%����)z��g����.���N��#S��b"�M5�Nc�k[����Te���r����Wy�%�g9O5~���=���3�M�!����������5����������#p�	h������
FR����]/��QQ�*����l��pB9��,b9��In���)�I��c	~-��b[Yt�Z�;yhW�������RIZIt|�7����������YtGlD$5�*���P^��a�W���.�~�2�������/�w�[���yE���[�s������PnG�vz|���1~U������)o4���&S����2�3��e��UG�J�+~b*����&"Sl�n��_Jk�|�S:�������6$S�*R���%�+���)Z#��Du
����A`�}�E0�QT@`�Q�.WD`�����4-���xw(�q)�ty�Z�5�$��Z�����)����D�����r�[�Fx%Ef�s4�Z�wfT��83��Tu�UA��w���?�b6��q��d���l:�+&��%��'�P.p2_^
?;;���Q��4�K�
b�@j\�O�����?IR����1q��I�Yu��u!\�VD���q1B+!�_����j(�M��u���-5Io���=���1�B��.T;%I��E�������:��
�O���	�	�Z�z����6Q
6����,�
F�d�Po��e�����)5p�9�Y�^��K Pt`��">b
���..H"��gPizc��l���J���U��q�q�i�!X��r��hdgP�:������&)�`���
�sn�|A	G���@���2��K�p�p�� Ac��Y��RZcW
�yL����Ma���p~�
!k������� �qe�&h�d�6*��jR[�*��.��H ie����Qvk�����J(]z��x��Fs���b�qA0�F�eU����P�NZ������)
VW-�8��m�q���5��NcX�H����(��x��a����5�T@�U��>H�6���-�4�.\�������`����r&�:<j8�Y���^EC
��
���N��K>"s����-E�;�Y���,����DUG7mJ�����f
qv}?�v��d*	����G^t*��x-��E�M
�	Qk�h�=O��8��7! ��������������KC�{�m��9j.�����\F����������������U��S5�H�DH2�@�0m"������[e�C��zB�+o~pDE������]�����3[�4<#�fk��S����m7��x��;��js�x� ���}e ��o�_f:����+U�g���@����P��:�����-�������-ET,8����i+q	R�&���Hb7)�=!�%����b!��"u��	��|C���y�d����Nm��`�c����Fb��\�bk���&�	���v�%z���O�LTG@����3e�L�"�1��p�D�j��� q.�iZY���;�vw�A���R��,�X�K�,��s1��m��F So�L��!�|G*c1�,������]w�T�m�!`U�z@%�������+�#�p��
i-��aL*�}gI�'�t�iS�+>����3�����-X}`�����iP����bgI�&T����
#24Antonin Houska
ah@cybertec.at
In reply to: Kenaniah Cerny (#23)
Re: Proposal: allow database-specific role memberships

Kenaniah Cerny <kenaniah@gmail.com> wrote:

Attached is a newly-rebased patch -- would love to get a review from someone whenever possible.

I've picked this patch for a review. The patch currently does not apply to the
master branch, so I could only read the diff. Following are my comments:

* I think that roles_is_member_of() deserves a comment explaining why the code
that you moved into append_role_memberships() needs to be called twice,
i.e. once for global memberships and once for the database-specific ones.

I think the reason is that if, for example, role "A" is a database-specific
member of role "B" and "B" is a "global" member of role "C", then "A" should
not be considered a member of "C", unless "A" is granted "C" explicitly. Is
this behavior intended?

Note that in this example, the "C" members are a superset of "B" members,
and thus "C" should have weaker permissions on database objects than
"B". What's then the reason to not consider "A" a member of "C"? If "C"
gives its members some permissions of "B" (e.g. "pg_write_all_data"), then I
think the roles hierarchy is poorly designed.

A counter-example might help me to understand.

* Why do you think that "unsafe_tests" is the appropriate name for the
directory that contains regression tests?

I can spend more time on the review if the patch gets rebased.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

#25Kenaniah Cerny
kenaniah@gmail.com
In reply to: Antonin Houska (#24)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Hi Antonin,

First of all, thank you so much for taking the time to review my patch.
I'll answer your questions in reverse order:

The "unsafe_tests" directory is where the pre-existing role tests were
located. According to the readme of the "unsafe_tests" directory, the tests
contained within are not run during "make installcheck" because they could
have side-effects that seem undesirable for a production installation. This
seemed like a reasonable location as the new tests that this patch
introduces also modifies the "state" of the database cluster by adding,
modifying, and removing roles & databases (including template1).

Regarding roles_is_member_of(), the nuance is that role "A" in your example
would only be considered a member of role "B" (and by extension role "C")
when connected to the database in which "A" was granted database-specific
membership to "B". Conversely, when connected to any other database, "A"
would not be considered to be a member of "B".

This patch is designed to solve the scenarios in which one may want to
grant constrained access to a broader set of privileges. For example,
membership in "pg_read_all_data" effectively grants SELECT and USAGE rights
on everything (implicitly cluster-wide in today's implementation). By
granting a role membership to "pg_read_all_data" within the context of a
specific database, the grantee's read-everything privilege is effectively
constrained to just that specific database (as membership within
"pg_read_all_data" would not otherwise be held).

A rebased version is attached.

Thanks again!

- Kenaniah

On Wed, Jun 29, 2022 at 6:45 AM Antonin Houska <ah@cybertec.at> wrote:

Show quoted text

Kenaniah Cerny <kenaniah@gmail.com> wrote:

Attached is a newly-rebased patch -- would love to get a review from

someone whenever possible.

I've picked this patch for a review. The patch currently does not apply to
the
master branch, so I could only read the diff. Following are my comments:

* I think that roles_is_member_of() deserves a comment explaining why the
code
that you moved into append_role_memberships() needs to be called twice,
i.e. once for global memberships and once for the database-specific ones.

I think the reason is that if, for example, role "A" is a
database-specific
member of role "B" and "B" is a "global" member of role "C", then "A"
should
not be considered a member of "C", unless "A" is granted "C" explicitly.
Is
this behavior intended?

Note that in this example, the "C" members are a superset of "B" members,
and thus "C" should have weaker permissions on database objects than
"B". What's then the reason to not consider "A" a member of "C"? If "C"
gives its members some permissions of "B" (e.g. "pg_write_all_data"),
then I
think the roles hierarchy is poorly designed.

A counter-example might help me to understand.

* Why do you think that "unsafe_tests" is the appropriate name for the
directory that contains regression tests?

I can spend more time on the review if the patch gets rebased.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

Attachments:

database-role-memberships-v9.patchapplication/octet-stream; name=database-role-memberships-v9.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 25b02c4e37ed..839f82050fe0 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1647,11 +1647,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>
@@ -1708,6 +1707,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 f744b05b55dc..c1d07b74d88f 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -98,6 +98,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> ]
 
@@ -251,7 +252,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>
@@ -277,6 +294,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>
 
@@ -398,10 +419,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 62f197103696..7e16e09087b0 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -127,6 +127,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 ]
 
@@ -305,6 +306,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 e784538aaea4..2b1657a2ec73 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -260,8 +260,9 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemDbMemRoleIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
 		relationId == DatabaseNameIndexId ||
 		relationId == DatabaseOidIndexId ||
 		relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f2691684010a..ac5024494bc5 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -47,6 +47,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"
@@ -1646,6 +1647,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 984305ba31cf..1e880873cf5c 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\"",
@@ -1485,16 +1496,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;
 		}
@@ -1508,6 +1526,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))
 		{
@@ -1548,7 +1567,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;
@@ -1575,7 +1594,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\"",
@@ -1594,14 +1613,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;
 		}
 
@@ -1643,3 +1669,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 51d630fa8925..237146ada786 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3970,6 +3970,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 e747e1667d01..5b84d4f03e95 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1571,6 +1571,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 969c9c158f63..daa04eb5e031 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -369,7 +369,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 parameter_name
 %type <list>	OptSchemaEltList parameter_name_list
@@ -7806,6 +7806,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; }
@@ -7818,21 +7823,22 @@ 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);
 
@@ -7840,10 +7846,11 @@ RevokeRoleStmt:
 					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);
 
@@ -7851,7 +7858,8 @@ RevokeRoleStmt:
 					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 b7fd3bcf057c..43c94a72d056 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4706,7 +4706,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -4740,7 +4740,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,
@@ -4789,6 +4789,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
  *
@@ -4806,7 +4848,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;
@@ -4855,37 +4897,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))
@@ -4937,7 +4957,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);
 }
 
@@ -4965,7 +4985,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);
 }
 
@@ -5003,7 +5023,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);
 }
 
@@ -5014,7 +5034,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;
 
@@ -5025,7 +5045,8 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(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;
 }
 
@@ -5101,7 +5122,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 6ae7c1f50b89..2a83c586fb42 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,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 f502df91dca5..c4b4945e4bca 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4086,7 +4086,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 1912b121463d..2cd084e7afe0 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -211,24 +211,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 26d3d53809ba..52ad68a5bf38 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -36,7 +36,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 dumpRoleGUCPrivs(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -564,7 +564,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 
 			/* Dump role GUC privileges */
 			if (server_version >= 150000 && !skip_acls)
@@ -921,7 +921,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -935,8 +935,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)
@@ -950,6 +951,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");
 
@@ -1319,7 +1322,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");
@@ -1330,6 +1333,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;
 
@@ -1370,6 +1374,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 16.0 or later */
+		if (server_version >= 160000)
+			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 d3dd8303d28c..72be6dbe2f72 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 f93d86654877..1b7b3b7ec200 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2439,6 +2439,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 48f7d72add5d..29043d569e62 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -211,7 +211,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 4463ea66bea5..8973ff6ebca9 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 df582736884f..9adc988e0220 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 guc_privs
+REGRESS = rolenames alter_system_table guc_privs 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..be79092b47ca
--- /dev/null
+++ b/src/test/modules/unsafe_tests/expected/role_membership.out
@@ -0,0 +1,541 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+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..c6cfde2ddcff
--- /dev/null
+++ b/src/test/modules/unsafe_tests/sql/role_membership.sql
@@ -0,0 +1,296 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+
+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}
#26Kenaniah Cerny
kenaniah@gmail.com
In reply to: Kenaniah Cerny (#25)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Rebased yet again...

On Mon, Jul 4, 2022 at 1:17 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

Show quoted text

Hi Antonin,

First of all, thank you so much for taking the time to review my patch.
I'll answer your questions in reverse order:

The "unsafe_tests" directory is where the pre-existing role tests were
located. According to the readme of the "unsafe_tests" directory, the tests
contained within are not run during "make installcheck" because they could
have side-effects that seem undesirable for a production installation. This
seemed like a reasonable location as the new tests that this patch
introduces also modifies the "state" of the database cluster by adding,
modifying, and removing roles & databases (including template1).

Regarding roles_is_member_of(), the nuance is that role "A" in your
example would only be considered a member of role "B" (and by extension
role "C") when connected to the database in which "A" was granted
database-specific membership to "B". Conversely, when connected to any
other database, "A" would not be considered to be a member of "B".

This patch is designed to solve the scenarios in which one may want to
grant constrained access to a broader set of privileges. For example,
membership in "pg_read_all_data" effectively grants SELECT and USAGE rights
on everything (implicitly cluster-wide in today's implementation). By
granting a role membership to "pg_read_all_data" within the context of a
specific database, the grantee's read-everything privilege is effectively
constrained to just that specific database (as membership within
"pg_read_all_data" would not otherwise be held).

A rebased version is attached.

Thanks again!

- Kenaniah

On Wed, Jun 29, 2022 at 6:45 AM Antonin Houska <ah@cybertec.at> wrote:

Kenaniah Cerny <kenaniah@gmail.com> wrote:

Attached is a newly-rebased patch -- would love to get a review from

someone whenever possible.

I've picked this patch for a review. The patch currently does not apply
to the
master branch, so I could only read the diff. Following are my comments:

* I think that roles_is_member_of() deserves a comment explaining why the
code
that you moved into append_role_memberships() needs to be called twice,
i.e. once for global memberships and once for the database-specific
ones.

I think the reason is that if, for example, role "A" is a
database-specific
member of role "B" and "B" is a "global" member of role "C", then "A"
should
not be considered a member of "C", unless "A" is granted "C"
explicitly. Is
this behavior intended?

Note that in this example, the "C" members are a superset of "B"
members,
and thus "C" should have weaker permissions on database objects than
"B". What's then the reason to not consider "A" a member of "C"? If "C"
gives its members some permissions of "B" (e.g. "pg_write_all_data"),
then I
think the roles hierarchy is poorly designed.

A counter-example might help me to understand.

* Why do you think that "unsafe_tests" is the appropriate name for the
directory that contains regression tests?

I can spend more time on the review if the patch gets rebased.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

Attachments:

database-role-memberships-v10.patchapplication/octet-stream; name=database-role-memberships-v10.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 670a5406d618..e91152c729a5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1647,11 +1647,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>
@@ -1708,6 +1707,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 f744b05b55dc..c1d07b74d88f 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -98,6 +98,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> ]
 
@@ -251,7 +252,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>
@@ -277,6 +294,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>
 
@@ -398,10 +419,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 62f197103696..7e16e09087b0 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -127,6 +127,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 ]
 
@@ -305,6 +306,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 6eaaaa36b881..94a2c6cb9e2e 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 6f43870779f6..058e3155c00f 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -260,8 +260,9 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemDbMemRoleIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
 		relationId == DatabaseNameIndexId ||
 		relationId == DatabaseOidIndexId ||
 		relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 099d369b2f4a..b9583cf06344 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -47,6 +47,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"
@@ -1643,6 +1644,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 5b24b6dcad80..ede6723c6d68 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 */
@@ -449,7 +449,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 			AddRoleMems(oldrolename, oldroleid,
 						thisrole_list,
 						thisrole_oidlist,
-						GetUserId(), false);
+						GetUserId(), false, InvalidOid);
 
 			ReleaseSysCache(oldroletup);
 		}
@@ -461,10 +461,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);
@@ -798,11 +798,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);
 	}
 
 	/*
@@ -1018,7 +1018,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)))
@@ -1033,7 +1033,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)))
@@ -1223,6 +1223,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);
@@ -1257,11 +1268,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);
 	}
 
 	/*
@@ -1368,7 +1379,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;
@@ -1395,7 +1406,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\"",
@@ -1478,16 +1489,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;
 		}
@@ -1497,6 +1515,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))
 		{
@@ -1537,7 +1556,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;
@@ -1564,7 +1583,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\"",
@@ -1583,14 +1602,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;
 		}
 
@@ -1628,3 +1654,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/parser/gram.y b/src/backend/parser/gram.y
index c018140afe41..c502a6b5dd46 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -369,7 +369,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 parameter_name
 %type <list>	OptSchemaEltList parameter_name_list
@@ -7816,6 +7816,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; }
@@ -7828,21 +7833,22 @@ 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);
 
@@ -7850,10 +7856,11 @@ RevokeRoleStmt:
 					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);
 
@@ -7861,7 +7868,8 @@ RevokeRoleStmt:
 					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 6fa58dd8eb07..7e7fb9d00efe 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4704,7 +4704,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -4738,7 +4738,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,
@@ -4787,6 +4787,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
  *
@@ -4804,7 +4846,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;
@@ -4853,37 +4895,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))
@@ -4935,7 +4955,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);
 }
 
@@ -4963,7 +4983,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);
 }
 
@@ -5001,7 +5021,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);
 }
 
@@ -5012,7 +5032,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;
 
@@ -5023,7 +5043,8 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(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;
 }
 
@@ -5099,7 +5120,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 6ae7c1f50b89..2a83c586fb42 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,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 bdb771d278f4..a41657e7ad26 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4086,7 +4086,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 1912b121463d..2cd084e7afe0 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -211,24 +211,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 26d3d53809ba..52ad68a5bf38 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -36,7 +36,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 dumpRoleGUCPrivs(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -564,7 +564,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 
 			/* Dump role GUC privileges */
 			if (server_version >= 150000 && !skip_acls)
@@ -921,7 +921,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -935,8 +935,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)
@@ -950,6 +951,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");
 
@@ -1319,7 +1322,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");
@@ -1330,6 +1333,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;
 
@@ -1370,6 +1374,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 16.0 or later */
+		if (server_version >= 160000)
+			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 d3dd8303d28c..72be6dbe2f72 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 98fe1abaa28a..dd7f4726cdd4 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2449,6 +2449,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 48f7d72add5d..29043d569e62 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -211,7 +211,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 4463ea66bea5..8973ff6ebca9 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 df582736884f..9adc988e0220 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 guc_privs
+REGRESS = rolenames alter_system_table guc_privs 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..be79092b47ca
--- /dev/null
+++ b/src/test/modules/unsafe_tests/expected/role_membership.out
@@ -0,0 +1,541 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+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..c6cfde2ddcff
--- /dev/null
+++ b/src/test/modules/unsafe_tests/sql/role_membership.sql
@@ -0,0 +1,296 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+
+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}
#27Antonin Houska
ah@cybertec.at
In reply to: Kenaniah Cerny (#26)
Re: Proposal: allow database-specific role memberships

Kenaniah Cerny <kenaniah@gmail.com> wrote:

Rebased yet again...

On Mon, Jul 4, 2022 at 1:17 PM Kenaniah Cerny <kenaniah@gmail.com> wrote:

The "unsafe_tests" directory is where the pre-existing role tests were
located. According to the readme of the "unsafe_tests" directory, the tests
contained within are not run during "make installcheck" because they could
have side-effects that seem undesirable for a production installation. This
seemed like a reasonable location as the new tests that this patch
introduces also modifies the "state" of the database cluster by adding,
modifying, and removing roles & databases (including template1).

ok, I missed the purpose of "unsafe_tests" so far, thanks.

Regarding roles_is_member_of(), the nuance is that role "A" in your example
would only be considered a member of role "B" (and by extension role "C")
when connected to the database in which "A" was granted database-specific
membership to "B".

Conversely, when connected to any other database, "A" would not be considered to be a member of "B".

This patch is designed to solve the scenarios in which one may want to
grant constrained access to a broader set of privileges. For example,
membership in "pg_read_all_data" effectively grants SELECT and USAGE rights
on everything (implicitly cluster-wide in today's implementation). By
granting a role membership to "pg_read_all_data" within the context of a
specific database, the grantee's read-everything privilege is effectively
constrained to just that specific database (as membership within
"pg_read_all_data" would not otherwise be held).

ok, I tried to view the problem rather from general perspective. However, the
permissions like "pg_read_all_data" are unusual in that they are rather strong
and at the same time they are usually located at the top of the groups
hierarchy. I've got no better idea how to solve the problem.

A few more comments on the patch:

* It's not clear from the explanation of the GRANT ... IN DATABASE ... / GRANT
... IN CURRENT DATABASE ... that, even if "membership in ... will be
effective only when the recipient is connected to the database ...", the
ADMIN option might not be "fully effective". I refer to the part of the
regression tests starting with

-- Ensure database-specific admin option can only grant within that database

For example, "role_read_34" does have the ADMIN option for the
"pg_read_all_data" role and for the "db_4" database:

GRANT pg_read_all_data TO role_read_34 IN DATABASE db_4 WITH ADMIN OPTION;

(in other words, "role_read_34" does have the database-specific membership
in "pg_read_all_data"), but it cannot use the option (in other words, cannot
use some ability resulting from that membership) unless the session to that
database is active:

\connect db_3
SET SESSION AUTHORIZATION role_read_34;
...
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"

Specifically on the regression tests:

* The function check_memberships() has no parameters - is there a reason not to use a view?

* I'm not sure if the pg_auth_members catalog can contain InvalidOid in
other columns than dbid. Thus I think that the query in
check_memberships() only needs an outer JOIN for the pg_database table,
while the other joins can be inner.

* In this part

SET SESSION AUTHORIZATION role_read_12_noinherit;
SELECT * FROM data; -- error
SET ROLE role_read_12; -- error
SELECT * FROM data; -- error

I think you don't need to query the table again if the SET ROLE statement
failed and the same query had been executed before the SET ROLE.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

#28Kenaniah Cerny
kenaniah@gmail.com
In reply to: Antonin Houska (#27)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Hi Antonin,

Thank you again for the detailed review and questions. It was encouraging
to see the increasing level of nuance in this latest round.

It's not clear from the explanation of the GRANT ... IN DATABASE ... / GRANT

... IN CURRENT DATABASE ... that, even if "membership in ... will be
effective only when the recipient is connected to the database ...", the
ADMIN option might not be "fully effective".

While I'm not entirely sure what you mean by fully effective, it sounds
like you may have expected a database-specific WITH ADMIN OPTION grant to
be able to take effect when connected to a different database (such as
being able to use db_4's database-specific grants when connected to db_3).
The documentation updated in this patch specifies that membership (for
database-specific grants) would be effective only when the grantee is
connected to the same database that the grant was issued for.

In the case of attempting to make a role grant to db_4 from within db_3,
the user would need to have a cluster-wide admin option for the role being
granted, as the test case you referenced in your example aims to verify.

I have added a couple of lines to the documentation included with this
patch in order to clarify.

Specifically on the regression tests:

* The function check_memberships() has no parameters - is there a
reason not to use a view?

I believe a view would work just as well -- this was an implementation
detail that was fashioned to match the pre-existing rolenames.sql file's
test format.

* I'm not sure if the pg_auth_members catalog can contain InvalidOid in
other columns than dbid. Thus I think that the query in
check_memberships() only needs an outer JOIN for the pg_database
table,
while the other joins can be inner.

This is probably true. The tests run just as well using inner joins for
pg_roles, as this latest version of the patch reflects.

* In this part

SET SESSION AUTHORIZATION role_read_12_noinherit;
SELECT * FROM data; -- error
SET ROLE role_read_12; -- error
SELECT * FROM data; -- error

I think you don't need to query the table again if the SET ROLE
statement
failed and the same query had been executed before the SET ROLE.

I left that last query in place as a sanity check to ensure that
role_read_12's privileges were indeed not in effect after the call to SET
ROLE.

As we appear to now be working through the minutiae, it is my hope that
this will soon be ready for merge.

- Kenaniah

Attachments:

database-role-memberships-v11.patchapplication/octet-stream; name=database-role-memberships-v11.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a186e35f0095..5598b0ea2967 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1647,11 +1647,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>
@@ -1708,6 +1707,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 f744b05b55dc..5a9d38e2b17e 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -98,6 +98,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> ]
 
@@ -251,7 +252,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>
@@ -259,7 +276,15 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    in turn grant membership in the role to others, and revoke membership
    in the role as well.  Without the admin option, ordinary users cannot
    do that.  A role is not considered to hold <literal>WITH ADMIN
-   OPTION</literal> on itself.  Database superusers can grant or revoke
+   OPTION</literal> on itself.  A cluster-wide admin option for the role being granted
+   is required for performing both cluster-wide role membership grants as well as
+   database-specific role membership grants when connected to a database that is not
+   the target of the grant.  A database-specific admin option is only sufficient for
+   performing a role membership grant that is both database-specific and for the
+   currently connected database.  Database-specific admin options can not be
+   used to grant cluster-wide role membership, nor can they be used to grant
+   database-specific role membership for other databases than the currently
+   connected database.  Database superusers can grant or revoke
    membership in any role to anyone.  Roles having
    <literal>CREATEROLE</literal> privilege can grant or revoke membership
    in any role that is not a superuser.
@@ -277,6 +302,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>
 
@@ -398,10 +427,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 62f197103696..7e16e09087b0 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -127,6 +127,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 ]
 
@@ -305,6 +306,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 6eaaaa36b881..94a2c6cb9e2e 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 6f43870779f6..058e3155c00f 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -260,8 +260,9 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemDbMemRoleIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
 		relationId == DatabaseNameIndexId ||
 		relationId == DatabaseOidIndexId ||
 		relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 099d369b2f4a..b9583cf06344 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -47,6 +47,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"
@@ -1643,6 +1644,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 5b24b6dcad80..ede6723c6d68 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 */
@@ -449,7 +449,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 			AddRoleMems(oldrolename, oldroleid,
 						thisrole_list,
 						thisrole_oidlist,
-						GetUserId(), false);
+						GetUserId(), false, InvalidOid);
 
 			ReleaseSysCache(oldroletup);
 		}
@@ -461,10 +461,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);
@@ -798,11 +798,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);
 	}
 
 	/*
@@ -1018,7 +1018,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)))
@@ -1033,7 +1033,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)))
@@ -1223,6 +1223,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);
@@ -1257,11 +1268,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);
 	}
 
 	/*
@@ -1368,7 +1379,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;
@@ -1395,7 +1406,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\"",
@@ -1478,16 +1489,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;
 		}
@@ -1497,6 +1515,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))
 		{
@@ -1537,7 +1556,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;
@@ -1564,7 +1583,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\"",
@@ -1583,14 +1602,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;
 		}
 
@@ -1628,3 +1654,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/parser/gram.y b/src/backend/parser/gram.y
index ace4fb5c7788..215df3062404 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -372,7 +372,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>		parameter_name
 %type <list>	OptSchemaEltList parameter_name_list
@@ -7836,6 +7836,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; }
@@ -7848,21 +7853,22 @@ 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);
 
@@ -7870,10 +7876,11 @@ RevokeRoleStmt:
 					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);
 
@@ -7881,7 +7888,8 @@ RevokeRoleStmt:
 					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 6fa58dd8eb07..7e7fb9d00efe 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4704,7 +4704,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -4738,7 +4738,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,
@@ -4787,6 +4787,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
  *
@@ -4804,7 +4846,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;
@@ -4853,37 +4895,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))
@@ -4935,7 +4955,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);
 }
 
@@ -4963,7 +4983,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);
 }
 
@@ -5001,7 +5021,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);
 }
 
@@ -5012,7 +5032,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;
 
@@ -5023,7 +5043,8 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(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;
 }
 
@@ -5099,7 +5120,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 38e943fab2bd..78adc2751ed9 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1105,7 +1105,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 bdb771d278f4..a41657e7ad26 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4086,7 +4086,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 1912b121463d..2cd084e7afe0 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -211,24 +211,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 26d3d53809ba..52ad68a5bf38 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -36,7 +36,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 dumpRoleGUCPrivs(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -564,7 +564,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 
 			/* Dump role GUC privileges */
 			if (server_version >= 150000 && !skip_acls)
@@ -921,7 +921,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PGresult   *res;
@@ -935,8 +935,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)
@@ -950,6 +951,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");
 
@@ -1319,7 +1322,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");
@@ -1330,6 +1333,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;
 
@@ -1370,6 +1374,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 16.0 or later */
+		if (server_version >= 160000)
+			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 d3dd8303d28c..72be6dbe2f72 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 98fe1abaa28a..dd7f4726cdd4 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2449,6 +2449,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 48f7d72add5d..29043d569e62 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -211,7 +211,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 4463ea66bea5..8973ff6ebca9 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 df582736884f..9adc988e0220 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 guc_privs
+REGRESS = rolenames alter_system_table guc_privs 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..511001688061
--- /dev/null
+++ b/src/test/modules/unsafe_tests/expected/role_membership.out
@@ -0,0 +1,541 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+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
+JOIN pg_roles r ON r.oid = a.roleid
+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..8157eeac2720
--- /dev/null
+++ b/src/test/modules/unsafe_tests/sql/role_membership.sql
@@ -0,0 +1,296 @@
+--
+-- Tests for database-specific role memberships.
+-- This is unsafe because roles and databases will added / removed / modified.
+--
+
+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
+JOIN pg_roles r ON r.oid = a.roleid
+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}
#29Ibrar Ahmed
ibrar.ahmad@gmail.com
In reply to: Kenaniah Cerny (#28)
Re: Proposal: allow database-specific role memberships

On Mon, Jul 25, 2022 at 4:03 AM Kenaniah Cerny <kenaniah@gmail.com> wrote:

Hi Antonin,

Thank you again for the detailed review and questions. It was encouraging
to see the increasing level of nuance in this latest round.

It's not clear from the explanation of the GRANT ... IN DATABASE ... /

GRANT
... IN CURRENT DATABASE ... that, even if "membership in ... will be
effective only when the recipient is connected to the database ...", the
ADMIN option might not be "fully effective".

While I'm not entirely sure what you mean by fully effective, it sounds
like you may have expected a database-specific WITH ADMIN OPTION grant to
be able to take effect when connected to a different database (such as
being able to use db_4's database-specific grants when connected to db_3).
The documentation updated in this patch specifies that membership (for
database-specific grants) would be effective only when the grantee is
connected to the same database that the grant was issued for.

In the case of attempting to make a role grant to db_4 from within db_3,
the user would need to have a cluster-wide admin option for the role being
granted, as the test case you referenced in your example aims to verify.

I have added a couple of lines to the documentation included with this
patch in order to clarify.

Specifically on the regression tests:

* The function check_memberships() has no parameters - is there a
reason not to use a view?

I believe a view would work just as well -- this was an implementation
detail that was fashioned to match the pre-existing rolenames.sql file's
test format.

* I'm not sure if the pg_auth_members catalog can contain InvalidOid
in
other columns than dbid. Thus I think that the query in
check_memberships() only needs an outer JOIN for the pg_database
table,
while the other joins can be inner.

This is probably true. The tests run just as well using inner joins for
pg_roles, as this latest version of the patch reflects.

* In this part

SET SESSION AUTHORIZATION role_read_12_noinherit;
SELECT * FROM data; -- error
SET ROLE role_read_12; -- error
SELECT * FROM data; -- error

I think you don't need to query the table again if the SET ROLE
statement
failed and the same query had been executed before the SET ROLE.

I left that last query in place as a sanity check to ensure that
role_read_12's privileges were indeed not in effect after the call to SET
ROLE.

As we appear to now be working through the minutiae, it is my hope that
this will soon be ready for merge.

- Kenaniah

The patch requires a rebase, please do that.

Hunk #5 succeeded at 454 (offset 28 lines). 1 out of 5 hunks FAILED
-- saving rejects to file doc/src/sgml/ref/grant.sgml.rej
...
...

--
Ibrar Ahmed

#30Michael Paquier
michael@paquier.xyz
In reply to: Ibrar Ahmed (#29)
Re: Proposal: allow database-specific role memberships

On Wed, Sep 07, 2022 at 12:50:32PM +0500, Ibrar Ahmed wrote:

The patch requires a rebase, please do that.

Hunk #5 succeeded at 454 (offset 28 lines). 1 out of 5 hunks FAILED
-- saving rejects to file doc/src/sgml/ref/grant.sgml.rej

There has been no updates on this thread for one month, so this has
been switched as RwF.
--
Michael

#31Denis Laxalde
denis.laxalde@dalibo.com
In reply to: Michael Paquier (#30)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Michael Paquier a écrit :

On Wed, Sep 07, 2022 at 12:50:32PM +0500, Ibrar Ahmed wrote:

The patch requires a rebase, please do that.

Hunk #5 succeeded at 454 (offset 28 lines). 1 out of 5 hunks FAILED
-- saving rejects to file doc/src/sgml/ref/grant.sgml.rej

There has been no updates on this thread for one month, so this has
been switched as RwF.

I took the liberty to rebase this (old) patch, originally authored by
Kenaniah Cerny.

This is about adding a "IN DATABASE <datname>" clause to GRANT and
REVOKE commands allowing to control role membership in a database scope,
rather that cluster-wise. This could be interesting in combination with
predefined roles, e.g.:

GRANT pg_read_all_data TO bob IN DATABASE app;
GRANT pg_maintain TO dba IN DATABASE metrics;

without having to grant too many privileges when a user is supposed to
only operate on some databases.

The logic of the original patch (as of its version 11) is preserved. One
noticeable change concerns tests: they got moved in src/test/regress
(there were in 'unsafe_tests'), with proper cleanup, and now avoid using
superuser as well as modifying templates.

Is this a feature that's still interesting? (Feedbacks, from 2022, in
the thread were a bit mixed.)

Personally, I have a few concerns regarding the feature and its
implementation:

- The IN DATABASE clause does not make much sense for some roles, like
pg_read_all_stats (the implementation does not guard against this).

- An 'IN SCHEMA' clause might be a natural supplementary feature.
However, the current implementation relying on a new 'dbid' column added
in pg_auth_members catalog might not fit well in that case.

Thanks,
Denis

Attachments:

Grant-revoke-role-membership-in-a-database-v12.patchtext/x-patch; charset=UTF-8; name=Grant-revoke-role-membership-in-a-database-v12.patchDownload
From 3ca8a488ed6059310eaec6824493843aa2ce52a3 Mon Sep 17 00:00:00 2001
From: Denis Laxalde <denis.laxalde@dalibo.com>
Date: Fri, 5 Jul 2024 15:54:16 +0200
Subject: [PATCH] Grant/revoke role membership in a database

This adds a 'IN DATABASE <datname>' clause to GRANT/REVOKE commands
allowing to control role membership in a database scope. A major use
case is probably in combination with predefined roles, e.g.:

  GRANT pg_read_all_data TO bob IN DATABASE app;
  GRANT pg_maintain TO dba IN DATABASE metrics;

to avoid granting too many privileges when a user is supposed to only
operate on a subset of the cluster's databases.

The information about which database a role membership applies to is
stored in a new 'dbid' column of pg_auth_members catalog. When this
column is 0 (InvalidOid), the membership applies cluster-wise. Indexes
on this table are modified in order to include the new column to match
actual queries.

In roles_is_member_of(), the membership lookup is now performed first
for the database at stake, and then possibly repeated without a database
id for the cluster-wise case. The repetition of this operation is
factored out into a new append_role_memberships() helper function where
the previous logic now lives.

Function has_privs_of_role() now accepts a database id, as last
argument. For cases where a cluster-wise membership is sought, e.g. for
roles like 'pg_read_server_files', the new has_cluster_privs_of_role()
macro is used. The is_admin_of_role() function is modified similarly.

Original author: Kenaniah Cerny <kenaniah@gmail.com>.
---
 .../basebackup_to_shell/basebackup_to_shell.c |   2 +-
 contrib/file_fdw/file_fdw.c                   |   4 +-
 .../pg_stat_statements/pg_stat_statements.c   |   2 +-
 contrib/pgrowlocks/pgrowlocks.c               |   2 +-
 doc/src/sgml/catalogs.sgml                    |  20 +-
 doc/src/sgml/ref/grant.sgml                   |  46 +-
 doc/src/sgml/ref/revoke.sgml                  |   9 +
 doc/src/sgml/user-manag.sgml                  |  14 +
 src/backend/backup/basebackup_server.c        |   2 +-
 src/backend/catalog/aclchk.c                  |  14 +-
 src/backend/catalog/catalog.c                 |   4 +-
 src/backend/catalog/namespace.c               |   2 +-
 src/backend/catalog/objectaddress.c           |   2 +-
 src/backend/commands/alter.c                  |   6 +-
 src/backend/commands/copy.c                   |   6 +-
 src/backend/commands/dbcommands.c             |   6 +
 src/backend/commands/indexcmds.c              |   4 +-
 src/backend/commands/subscriptioncmds.c       |   2 +-
 src/backend/commands/user.c                   | 148 +++-
 src/backend/parser/gram.y                     |  33 +-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |   2 +-
 src/backend/rewrite/rowsecurity.c             |   2 +-
 src/backend/storage/ipc/procarray.c           |   4 +-
 src/backend/storage/ipc/signalfuncs.c         |   6 +-
 src/backend/tcop/utility.c                    |   2 +-
 src/backend/utils/adt/acl.c                   | 147 ++--
 src/backend/utils/adt/dbsize.c                |   4 +-
 src/backend/utils/adt/genfile.c               |   2 +-
 src/backend/utils/adt/pgstatfuncs.c           |   2 +-
 src/backend/utils/cache/catcache.c            |   2 +-
 src/backend/utils/cache/relcache.c            |   2 +-
 src/backend/utils/init/postinit.c             |   2 +-
 src/backend/utils/misc/guc_funcs.c            |   4 +-
 src/bin/pg_dump/pg_dumpall.c                  |  18 +-
 src/include/catalog/pg_auth_members.h         |  10 +-
 src/include/commands/user.h                   |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/include/utils/acl.h                       |   7 +-
 src/test/regress/expected/oidjoins.out        |   1 +
 src/test/regress/expected/privs_in_db.out     | 767 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/privs_in_db.sql          | 370 +++++++++
 43 files changed, 1518 insertions(+), 172 deletions(-)
 create mode 100644 src/test/regress/expected/privs_in_db.out
 create mode 100644 src/test/regress/sql/privs_in_db.sql

diff --git a/contrib/basebackup_to_shell/basebackup_to_shell.c b/contrib/basebackup_to_shell/basebackup_to_shell.c
index fe419d9080..d35e2f61c2 100644
--- a/contrib/basebackup_to_shell/basebackup_to_shell.c
+++ b/contrib/basebackup_to_shell/basebackup_to_shell.c
@@ -104,7 +104,7 @@ shell_check_detail(char *target, char *target_detail)
 
 		StartTransactionCommand();
 		roleid = get_role_oid(shell_required_role, true);
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_cluster_privs_of_role(GetUserId(), roleid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to use basebackup_to_shell")));
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index d16821f8e1..7048baa9d0 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -276,7 +276,7 @@ file_fdw_validator(PG_FUNCTION_ARGS)
 			 * otherwise there'd still be a security hole.
 			 */
 			if (strcmp(def->defname, "filename") == 0 &&
-				!has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+				!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to set the \"%s\" option of a file_fdw foreign table",
@@ -285,7 +285,7 @@ file_fdw_validator(PG_FUNCTION_ARGS)
 								   "pg_read_server_files")));
 
 			if (strcmp(def->defname, "program") == 0 &&
-				!has_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
+				!has_cluster_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to set the \"%s\" option of a file_fdw foreign table",
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 3c72e437f7..4e43a8bbcc 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -1644,7 +1644,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 	 * Superusers or roles with the privileges of pg_read_all_stats members
 	 * are allowed
 	 */
-	is_allowed_role = has_privs_of_role(userid, ROLE_PG_READ_ALL_STATS);
+	is_allowed_role = has_cluster_privs_of_role(userid, ROLE_PG_READ_ALL_STATS);
 
 	/* hash table must exist already */
 	if (!pgss || !pgss_hash)
diff --git a/contrib/pgrowlocks/pgrowlocks.c b/contrib/pgrowlocks/pgrowlocks.c
index adbc8279c3..3c82e63478 100644
--- a/contrib/pgrowlocks/pgrowlocks.c
+++ b/contrib/pgrowlocks/pgrowlocks.c
@@ -104,7 +104,7 @@ pgrowlocks(PG_FUNCTION_ARGS)
 	aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
 								  ACL_SELECT);
 	if (aclresult != ACLCHECK_OK)
-		aclresult = has_privs_of_role(GetUserId(), ROLE_PG_STAT_SCAN_TABLES) ? ACLCHECK_OK : ACLCHECK_NO_PRIV;
+		aclresult = has_privs_of_role(GetUserId(), ROLE_PG_STAT_SCAN_TABLES, MyDatabaseId) ? ACLCHECK_OK : ACLCHECK_NO_PRIV;
 
 	if (aclresult != ACLCHECK_OK)
 		aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bfb97865e1..927df5caa4 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1648,11 +1648,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>
@@ -1739,6 +1738,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        to the granted role
       </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 999f657d5c..b53cae4c14 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -98,6 +98,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 | INHERIT | SET } { OPTION | TRUE | FALSE } ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
@@ -262,6 +263,27 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    option values.
   </para>
 
+  <para>
+   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>
    Each of the options described below can be set to either
    <literal>TRUE</literal> or <literal>FALSE</literal>. The keyword
@@ -279,7 +301,15 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    do that.  A role is not considered to hold <literal>WITH ADMIN
    OPTION</literal> on itself.  Database superusers can grant or revoke
    membership in any role to anyone. This option defaults to
-   <literal>FALSE</literal>.
+   <literal>FALSE</literal>.  A cluster-wide admin option for the role being granted
+   is required for performing both cluster-wide role membership grants as well as
+   database-specific role membership grants when connected to a database that is not
+   the target of the grant.  A database-specific admin option is only sufficient for
+   performing a role membership grant that is both database-specific and for the
+   currently connected database.  Database-specific admin options can not be
+   used to grant cluster-wide role membership, nor can they be used to grant
+   database-specific role membership for other databases than the currently
+   connected database.
   </para>
 
   <para>
@@ -335,6 +365,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>
 
@@ -456,10 +490,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 8df492281a..00fce4997b 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -127,6 +127,7 @@ REVOKE [ GRANT OPTION FOR ]
 
 REVOKE [ { ADMIN | INHERIT | SET } 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 ]
 
@@ -316,6 +317,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 0a005481d1..abcf8eceb2 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -391,6 +391,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
@@ -598,6 +603,15 @@ 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>
+
   <warning>
    <para>
     Care should be taken when granting these roles to ensure they are only used
diff --git a/src/backend/backup/basebackup_server.c b/src/backend/backup/basebackup_server.c
index f5c0c61640..ae0ed71ffc 100644
--- a/src/backend/backup/basebackup_server.c
+++ b/src/backend/backup/basebackup_server.c
@@ -67,7 +67,7 @@ bbsink_server_new(bbsink *next, char *pathname)
 
 	/* Replication permission is not sufficient in this case. */
 	StartTransactionCommand();
-	if (!has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
+	if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to create backup stored on server"),
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index d2abc48fd8..d50dca2c31 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -1141,7 +1141,7 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 
 			iacls.roleid = get_rolespec_oid(rolespec, false);
 
-			if (!has_privs_of_role(GetUserId(), iacls.roleid))
+			if (!has_privs_of_role(GetUserId(), iacls.roleid, MyDatabaseId))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to change default privileges")));
@@ -3425,7 +3425,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 * pg_read_all_data role, which allows read access to all relations.
 	 */
 	if (mask & ACL_SELECT && !(result & ACL_SELECT) &&
-		has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA))
+		has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA, MyDatabaseId))
 		result |= ACL_SELECT;
 
 	/*
@@ -3437,7 +3437,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 */
 	if (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE) &&
 		!(result & (ACL_INSERT | ACL_UPDATE | ACL_DELETE)) &&
-		has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA))
+		has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA, MyDatabaseId))
 		result |= (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE));
 
 	/*
@@ -3448,7 +3448,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 */
 	if (mask & ACL_MAINTAIN &&
 		!(result & ACL_MAINTAIN) &&
-		has_privs_of_role(roleid, ROLE_PG_MAINTAIN))
+		has_privs_of_role(roleid, ROLE_PG_MAINTAIN, MyDatabaseId))
 		result |= ACL_MAINTAIN;
 
 	return result;
@@ -3747,8 +3747,8 @@ pg_namespace_aclmask_ext(Oid nsp_oid, Oid roleid,
 	 * to all schemas.
 	 */
 	if (mask & ACL_USAGE && !(result & ACL_USAGE) &&
-		(has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA) ||
-		 has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA)))
+		(has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA, MyDatabaseId) ||
+		 has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA, MyDatabaseId)))
 		result |= ACL_USAGE;
 	return result;
 }
@@ -4203,7 +4203,7 @@ object_ownercheck(Oid classid, Oid objectid, Oid roleid)
 		table_close(rel, AccessShareLock);
 	}
 
-	return has_privs_of_role(roleid, ownerId);
+	return has_privs_of_role(roleid, ownerId, MyDatabaseId);
 }
 
 /*
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index d6b07a7865..9babe691d6 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -279,8 +279,8 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
 		relationId == AuthMemOidIndexId ||
 		relationId == AuthMemGrantorIndexId ||
 		relationId == DatabaseNameIndexId ||
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 43b707699d..eb3d2aa386 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -4763,7 +4763,7 @@ InitializeSearchPath(void)
 									  (Datum) 0);
 
 		/* role membership may affect ACLs */
-		CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
+		CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB,
 									  InvalidationCallback,
 									  (Datum) 0);
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 85a7b7e641..099d06c832 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2521,7 +2521,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 							 errmsg("permission denied"),
 							 errdetail("The current user must have the %s attribute.",
 									   "CREATEROLE")));
-				if (!is_admin_of_role(roleid, address.objectId))
+				if (!is_admin_of_role(roleid, address.objectId, InvalidOid))
 					ereport(ERROR,
 							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 							 errmsg("permission denied"),
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb447..3de2a3be9d 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -219,7 +219,7 @@ AlterObjectRename_internal(Relation rel, Oid objectId, const char *new_name)
 		Assert(!isnull);
 		ownerId = DatumGetObjectId(datum);
 
-		if (!has_privs_of_role(GetUserId(), DatumGetObjectId(ownerId)))
+		if (!has_privs_of_role(GetUserId(), DatumGetObjectId(ownerId), MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_object_type(classId, objectId),
 						   old_name);
 
@@ -739,7 +739,7 @@ AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid)
 		Assert(!isnull);
 		ownerId = DatumGetObjectId(owner);
 
-		if (!has_privs_of_role(GetUserId(), ownerId))
+		if (!has_privs_of_role(GetUserId(), ownerId, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_object_type(classId, objid),
 						   NameStr(*(DatumGetName(name))));
 
@@ -961,7 +961,7 @@ AlterObjectOwner_internal(Oid classId, Oid objectId, Oid new_ownerId)
 		if (!superuser())
 		{
 			/* must be owner */
-			if (!has_privs_of_role(GetUserId(), old_ownerId))
+			if (!has_privs_of_role(GetUserId(), old_ownerId, MyDatabaseId))
 			{
 				char	   *objname;
 				char		namebuf[NAMEDATALEN];
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 3bb579a3a4..bd91ef5983 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -78,7 +78,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		if (stmt->is_program)
 		{
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY to or from an external program"),
@@ -89,7 +89,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		}
 		else
 		{
-			if (is_from && !has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+			if (is_from && !has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY from a file"),
@@ -98,7 +98,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 						 errhint("Anyone can COPY to stdout or from stdin. "
 								 "psql's \\copy command also works for anyone.")));
 
-			if (!is_from && !has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
+			if (!is_from && !has_cluster_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY to a file"),
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 8be435a79e..792dd5be41 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -48,6 +48,7 @@
 #include "commands/defrem.h"
 #include "commands/seclabel.h"
 #include "commands/tablespace.h"
+#include "commands/user.h"
 #include "common/file_perm.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
@@ -1774,6 +1775,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/indexcmds.c b/src/backend/commands/indexcmds.c
index f99c2d2dee..f13c35b4c5 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -3146,7 +3146,7 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
 		objectOid = get_namespace_oid(objectName, false);
 
 		if (!object_ownercheck(NamespaceRelationId, objectOid, GetUserId()) &&
-			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
+			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SCHEMA,
 						   objectName);
 	}
@@ -3159,7 +3159,7 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("can only reindex the currently open database")));
 		if (!object_ownercheck(DatabaseRelationId, objectOid, GetUserId()) &&
-			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
+			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
 						   get_database_name(objectOid));
 	}
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 02ccc636b8..670590470e 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -611,7 +611,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 	 * attempts to access arbitrary network destinations, so require the user
 	 * to have been specifically authorized to create subscriptions.
 	 */
-	if (!has_privs_of_role(owner, ROLE_PG_CREATE_SUBSCRIPTION))
+	if (!has_privs_of_role(owner, ROLE_PG_CREATE_SUBSCRIPTION, MyDatabaseId))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to create subscription"),
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index e7ade898a4..f56cc3eaba 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -92,15 +92,15 @@ check_password_hook_type check_password_hook = NULL;
 
 static void AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
-						Oid grantorId, GrantRoleOptions *popt);
+						Oid grantorId, GrantRoleOptions *popt, Oid db_id);
 static void DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
 						Oid grantorId, GrantRoleOptions *popt,
-						DropBehavior behavior);
+						DropBehavior behavior, Oid db_id);
 static void check_role_membership_authorization(Oid currentUserId, Oid roleid,
-												bool is_grant);
+												bool is_grant, Oid db_id);
 static Oid	check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
-							   bool is_grant);
+							   bool is_grant, Oid db_id);
 static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
 static bool plan_single_revoke(CatCList *memlist,
 							   RevokeRoleGrantAction *actions,
@@ -517,11 +517,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 			char	   *oldrolename = NameStr(oldroleform->rolname);
 
 			/* can only add this role to roles for which you have rights */
-			check_role_membership_authorization(currentUserId, oldroleid, true);
+			check_role_membership_authorization(currentUserId, oldroleid,
+												true, InvalidOid);
 			AddRoleMems(currentUserId, oldrolename, oldroleid,
 						thisrole_list,
 						thisrole_oidlist,
-						InvalidOid, &popt);
+						InvalidOid, &popt, InvalidOid);
 
 			ReleaseSysCache(oldroletup);
 		}
@@ -557,7 +558,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 		AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
 					memberSpecs, memberIds,
-					BOOTSTRAP_SUPERUSERID, &poptself);
+					BOOTSTRAP_SUPERUSERID, &poptself, InvalidOid);
 
 		/*
 		 * We must make the implicit grant visible to the code below, else the
@@ -577,7 +578,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		if (createrole_self_grant_enabled)
 			AddRoleMems(currentUserId, stmt->role, roleid,
 						memberSpecs, memberIds,
-						currentUserId, &createrole_self_grant_options);
+						currentUserId, &createrole_self_grant_options, InvalidOid);
 	}
 
 	/*
@@ -589,12 +590,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	AddRoleMems(currentUserId, stmt->role, roleid,
 				rolemembers, roleSpecsToIds(rolemembers),
-				InvalidOid, &popt);
+				InvalidOid, &popt, InvalidOid);
 	popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
 	popt.admin = true;
 	AddRoleMems(currentUserId, stmt->role, roleid,
 				adminmembers, roleSpecsToIds(adminmembers),
-				InvalidOid, &popt);
+				InvalidOid, &popt, InvalidOid);
 
 	/* Post creation hook for new role */
 	InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -771,7 +772,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	 * and also ADMIN OPTION on the role.
 	 */
 	if (!have_createrole_privilege() ||
-		!is_admin_of_role(GetUserId(), roleid))
+		!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 	{
 		/* things an unprivileged user certainly can't do */
 		if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
@@ -818,7 +819,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 
 	/* To add members to a role, you need ADMIN OPTION. */
-	if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+	if (drolemembers && !is_admin_of_role(currentUserId, roleid, InvalidOid))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to alter role"),
@@ -977,11 +978,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		if (stmt->action == +1) /* add members to role */
 			AddRoleMems(currentUserId, rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						InvalidOid, &popt);
+						InvalidOid, &popt, InvalidOid);
 		else if (stmt->action == -1)	/* drop members from role */
 			DelRoleMems(currentUserId, rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						InvalidOid, &popt, DROP_RESTRICT);
+						InvalidOid, &popt, DROP_RESTRICT, InvalidOid);
 	}
 
 	/*
@@ -1036,7 +1037,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 		else
 		{
 			if ((!have_createrole_privilege() ||
-				 !is_admin_of_role(GetUserId(), roleid))
+				 !is_admin_of_role(GetUserId(), roleid, InvalidOid))
 				&& roleid != GetUserId())
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1171,7 +1172,7 @@ DropRole(DropRoleStmt *stmt)
 					 errmsg("permission denied to drop role"),
 					 errdetail("Only roles with the %s attribute may drop roles with the %s attribute.",
 							   "SUPERUSER", "SUPERUSER")));
-		if (!is_admin_of_role(GetUserId(), roleid))
+		if (!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to drop role"),
@@ -1212,7 +1213,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)))
@@ -1232,7 +1233,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)))
@@ -1425,7 +1426,7 @@ RenameRole(const char *oldname, const char *newname)
 	else
 	{
 		if (!have_createrole_privilege() ||
-			!is_admin_of_role(GetUserId(), roleid))
+			!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to rename role"),
@@ -1485,6 +1486,7 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 	ListCell   *item;
 	GrantRoleOptions popt;
 	Oid			currentUserId = GetUserId();
+	Oid			dbid;
 
 	/* Parse options list. */
 	InitGrantRoleOptions(&popt);
@@ -1533,6 +1535,14 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 
 	grantee_ids = roleSpecsToIds(stmt->grantee_roles);
 
+	/* Lookup OID of database, if specified. */
+	if (stmt->database == NULL)
+		dbid = InvalidOid;
+	else if (strcmp(stmt->database, "") == 0)
+		dbid = MyDatabaseId;
+	else
+		dbid = get_database_oid(stmt->database, false);
+
 	/* AccessShareLock is enough since we aren't modifying pg_authid */
 	pg_authid_rel = table_open(AuthIdRelationId, AccessShareLock);
 
@@ -1557,15 +1567,15 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 
 		roleid = get_role_oid(rolename, false);
 		check_role_membership_authorization(currentUserId,
-											roleid, stmt->is_grant);
+											roleid, stmt->is_grant, dbid);
 		if (stmt->is_grant)
 			AddRoleMems(currentUserId, rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						grantor, &popt);
+						grantor, &popt, dbid);
 		else
 			DelRoleMems(currentUserId, rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						grantor, &popt, stmt->behavior);
+						grantor, &popt, stmt->behavior, dbid);
 	}
 
 	/*
@@ -1590,7 +1600,7 @@ DropOwnedObjects(DropOwnedStmt *stmt)
 	{
 		Oid			roleid = lfirst_oid(cell);
 
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_privs_of_role(GetUserId(), roleid, MyDatabaseId))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to drop objects"),
@@ -1619,7 +1629,7 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
 	{
 		Oid			roleid = lfirst_oid(cell);
 
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_privs_of_role(GetUserId(), roleid, MyDatabaseId))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to reassign objects"),
@@ -1630,7 +1640,7 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
 	/* Must have privileges on the receiving side too */
 	newrole = get_rolespec_oid(stmt->newrole, false);
 
-	if (!has_privs_of_role(GetUserId(), newrole))
+	if (!has_privs_of_role(GetUserId(), newrole, MyDatabaseId))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to reassign objects"),
@@ -1676,11 +1686,12 @@ roleSpecsToIds(List *memberNames)
  * grantorId: OID that should be recorded as having granted the membership
  * (InvalidOid if not set explicitly)
  * popt: information about grant options
+ * db_id: OID of the database in which membership should be granted
  */
 static void
 AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			Oid grantorId, GrantRoleOptions *popt)
+			Oid grantorId, GrantRoleOptions *popt, Oid db_id)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1690,7 +1701,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	Assert(list_length(memberSpecs) == list_length(memberIds));
 
 	/* Validate grantor (and resolve implicit grantor if not specified). */
-	grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
+	grantorId = check_role_grantor(currentUserId, roleid, grantorId, true, db_id);
 
 	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
 	pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -1769,7 +1780,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 		int			i;
 
 		/* Get the list of members for this role. */
-		memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+		memlist = SearchSysCacheList1(AUTHMEMROLEMEMDB,
 									  ObjectIdGetDatum(roleid));
 
 		/*
@@ -1834,12 +1845,15 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			ObjectIdGetDatum(memberid);
 		new_record[Anum_pg_auth_members_grantor - 1] =
 			ObjectIdGetDatum(grantorId);
+		new_record[Anum_pg_auth_members_dbid - 1] =
+			ObjectIdGetDatum(db_id);
 
 		/* Find any existing tuple */
-		authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
+		authmem_tuple = SearchSysCache4(AUTHMEMROLEMEMDB,
 										ObjectIdGetDatum(roleid),
 										ObjectIdGetDatum(memberid),
-										ObjectIdGetDatum(grantorId));
+										ObjectIdGetDatum(grantorId),
+										ObjectIdGetDatum(db_id));
 
 		/*
 		 * If we found a tuple, update it with new option values, unless there
@@ -1886,10 +1900,16 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 
 			if (!at_least_one_change)
 			{
-				ereport(NOTICE,
-						(errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
-								get_rolespec_name(memberRole), rolename,
-								GetUserNameFromId(grantorId, false))));
+				if (OidIsValid(db_id))
+					ereport(NOTICE,
+							(errmsg("role \"%s\" has already been granted membership in role \"%s\" in database \"%s\" by role \"%s\"",
+									get_rolespec_name(memberRole), rolename, get_database_name(db_id),
+									GetUserNameFromId(grantorId, false))));
+				else
+					ereport(NOTICE,
+							(errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+									get_rolespec_name(memberRole), rolename,
+									GetUserNameFromId(grantorId, false))));
 				ReleaseSysCache(authmem_tuple);
 				continue;
 			}
@@ -1974,11 +1994,12 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
  * grantorId: who is revoking the membership
  * popt: information about grant options
  * behavior: RESTRICT or CASCADE behavior for recursive removal
+ * db_id: OID of the database in which membership should be removed
  */
 static void
 DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
+			Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior, Oid db_id)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1991,7 +2012,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	Assert(list_length(memberSpecs) == list_length(memberIds));
 
 	/* Validate grantor (and resolve implicit grantor if not specified). */
-	grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+	grantorId = check_role_grantor(currentUserId, roleid, grantorId, false, db_id);
 
 	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
 	pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -2004,7 +2025,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	LockSharedObject(AuthIdRelationId, roleid, 0,
 					 ShareUpdateExclusiveLock);
 
-	memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+	memlist = SearchSysCacheList1(AUTHMEMROLEMEMDB, ObjectIdGetDatum(roleid));
 	actions = initialize_revoke_actions(memlist);
 
 	/*
@@ -2109,7 +2130,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
  */
 static void
 check_role_membership_authorization(Oid currentUserId, Oid roleid,
-									bool is_grant)
+									bool is_grant, Oid dbid)
 {
 	/*
 	 * The charter of pg_database_owner is to have exactly one, implicit,
@@ -2152,7 +2173,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
 		/*
 		 * Otherwise, must have admin option on the role to be changed.
 		 */
-		if (!is_admin_of_role(currentUserId, roleid))
+		if (!is_admin_of_role(currentUserId, roleid, dbid))
 		{
 			if (is_grant)
 				ereport(ERROR,
@@ -2202,7 +2223,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
  * the operation.
  */
 static Oid
-check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant, Oid db_id)
 {
 	/* If the grantor ID was not specified, pick one to use. */
 	if (!OidIsValid(grantorId))
@@ -2226,7 +2247,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 		 * established that the current user has permission to perform the
 		 * operation.)
 		 */
-		grantorId = select_best_admin(currentUserId, roleid);
+		grantorId = select_best_admin(currentUserId, roleid, db_id);
 		if (!OidIsValid(grantorId))
 			elog(ERROR, "no possible grantors");
 		return grantorId;
@@ -2243,7 +2264,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 	 */
 	if (is_grant)
 	{
-		if (!has_privs_of_role(currentUserId, grantorId))
+		if (!has_privs_of_role(currentUserId, grantorId, db_id))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to grant privileges as role \"%s\"",
@@ -2252,7 +2273,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 							   GetUserNameFromId(grantorId, false))));
 
 		if (grantorId != BOOTSTRAP_SUPERUSERID &&
-			select_best_admin(grantorId, roleid) != grantorId)
+			select_best_admin(grantorId, roleid, db_id) != grantorId)
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to grant privileges as role \"%s\"",
@@ -2262,7 +2283,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 	}
 	else
 	{
-		if (!has_privs_of_role(currentUserId, grantorId))
+		if (!has_privs_of_role(currentUserId, grantorId, db_id))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to revoke privileges granted by role \"%s\"",
@@ -2579,3 +2600,42 @@ assign_createrole_self_grant(const char *newval, void *extra)
 	createrole_self_grant_options.set =
 		(options & GRANT_ROLE_SPECIFIED_SET) != 0;
 }
+
+/*
+ * DropDatabaseSpecificRoles
+ *
+ * Delete pg_auth_members entries corresponding to a database that's being
+ * dropped.
+ */
+void
+DropDatabaseSpecificRoles(Oid databaseId)
+{
+	Relation	pg_authmem_rel;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+	HeapTuple	tup;
+
+	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_members_dbid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(databaseId));
+
+	sscan = systable_beginscan(pg_authmem_rel, AuthMemMemRoleDbIndexId,
+							   true, NULL, 1, &scankey);
+
+	while (HeapTupleIsValid(tup = systable_getnext(sscan)))
+	{
+		Form_pg_auth_members authmem_form;
+
+		authmem_form = (Form_pg_auth_members) GETSTRUCT(tup);
+		deleteSharedDependencyRecordsFor(AuthMemRelationId,
+										 authmem_form->oid, 0);
+		CatalogTupleDelete(pg_authmem_rel, &tup->t_self);
+	}
+
+	systable_endscan(sscan);
+
+	table_close(pg_authmem_rel, RowExclusiveLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b1d4642c59..9789dc5856 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -367,7 +367,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>		parameter_name
 %type <list>	OptSchemaEltList parameter_name_list
@@ -7906,6 +7906,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; }
@@ -7918,32 +7923,34 @@ opt_grant_grant_option:
  *****************************************************************************/
 
 GrantRoleStmt:
-			GRANT privilege_list TO role_list opt_granted_by
+			GRANT privilege_list TO role_list opt_grant_in_database opt_granted_by
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 
 					n->is_grant = true;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
+					n->database = $5;
 					n->opt = NIL;
-					n->grantor = $5;
+					n->grantor = $6;
 					$$ = (Node *) n;
 				}
-		  | GRANT privilege_list TO role_list WITH grant_role_opt_list opt_granted_by
+		  | GRANT privilege_list TO role_list opt_grant_in_database WITH grant_role_opt_list opt_granted_by
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 
 					n->is_grant = true;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->opt = $6;
-					n->grantor = $7;
+					n->database = $5;
+					n->opt = $7;
+					n->grantor = $8;
 					$$ = (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);
 
@@ -7951,11 +7958,12 @@ RevokeRoleStmt:
 					n->opt = NIL;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->grantor = $5;
-					n->behavior = $6;
+					n->database = $5;
+					n->grantor = $6;
+					n->behavior = $7;
 					$$ = (Node *) n;
 				}
-			| REVOKE ColId OPTION FOR privilege_list FROM role_list opt_granted_by opt_drop_behavior
+			| REVOKE ColId OPTION FOR privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 					DefElem *opt;
@@ -7966,8 +7974,9 @@ RevokeRoleStmt:
 					n->opt = list_make1(opt);
 					n->granted_roles = $5;
 					n->grantee_roles = $7;
-					n->grantor = $8;
-					n->behavior = $9;
+					n->database = $8;
+					n->grantor = $9;
+					n->behavior = $10;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index a27aee63de..4f3afe069c 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -1465,7 +1465,7 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS)
 	/* Fetch values */
 	values[0] = Int32GetDatum(pid);
 
-	if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		/*
 		 * Only superusers and roles with privileges of pg_read_all_stats can
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index c5f1009f37..d5e100bf2e 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -3936,7 +3936,7 @@ pg_stat_get_wal_senders(PG_FUNCTION_ARGS)
 
 		values[0] = Int32GetDatum(pid);
 
-		if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 		{
 			/*
 			 * Only superusers and roles with privileges of pg_read_all_stats
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index 59fd305dd7..84cace07f9 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -924,7 +924,7 @@ check_role_for_policy(ArrayType *policy_roles, Oid user_id)
 
 	for (i = 0; i < ARR_DIMS(policy_roles)[0]; i++)
 	{
-		if (has_privs_of_role(user_id, roles[i]))
+		if (has_privs_of_role(user_id, roles[i], MyDatabaseId))
 			return true;
 	}
 
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 36610a1c7e..77558d4c88 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -3899,8 +3899,8 @@ TerminateOtherDBBackends(Oid databaseId)
 							 errdetail("Only roles with the %s attribute may terminate processes of roles with the %s attribute.",
 									   "SUPERUSER", "SUPERUSER")));
 
-				if (!has_privs_of_role(GetUserId(), proc->roleId) &&
-					!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
+				if (!has_privs_of_role(GetUserId(), proc->roleId, databaseId) &&
+					!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND, databaseId))
 					ereport(ERROR,
 							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 							 errmsg("permission denied to terminate process"),
diff --git a/src/backend/storage/ipc/signalfuncs.c b/src/backend/storage/ipc/signalfuncs.c
index aa729a36e3..50e6ed3a50 100644
--- a/src/backend/storage/ipc/signalfuncs.c
+++ b/src/backend/storage/ipc/signalfuncs.c
@@ -92,14 +92,14 @@ pg_signal_backend(int pid, int sig)
 
 		if (procStatus && procStatus->st_backendType == B_AUTOVAC_WORKER)
 		{
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_AUTOVACUUM_WORKER))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_AUTOVACUUM_WORKER))
 				return SIGNAL_BACKEND_NOAUTOVAC;
 		}
 		else if (!superuser())
 			return SIGNAL_BACKEND_NOSUPERUSER;
 	}
-	else if (!has_privs_of_role(GetUserId(), proc->roleId) &&
-			 !has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
+	else if (!has_cluster_privs_of_role(GetUserId(), proc->roleId) &&
+			 !has_cluster_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
 		return SIGNAL_BACKEND_NOPERMISSION;
 
 	/*
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index b2ea8125c9..900ccc292d 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -940,7 +940,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_CheckPointStmt:
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				/* translator: %s is name of a SQL command, eg CHECKPOINT */
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 2a716cc6b7..d2f186da24 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -1395,7 +1395,7 @@ aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 
 	/* Owner always implicitly has all grant options */
 	if ((mask & ACLITEM_ALL_GOPTION_BITS) &&
-		has_privs_of_role(roleid, ownerId))
+		has_privs_of_role(roleid, ownerId, MyDatabaseId))
 	{
 		result = mask & ACLITEM_ALL_GOPTION_BITS;
 		if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
@@ -1438,7 +1438,7 @@ aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 			continue;			/* already checked it */
 
 		if ((aidata->ai_privs & remaining) &&
-			has_privs_of_role(roleid, aidata->ai_grantee))
+			has_privs_of_role(roleid, aidata->ai_grantee, MyDatabaseId))
 		{
 			result |= aidata->ai_privs & mask;
 			if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
@@ -4995,7 +4995,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -5005,7 +5005,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 	}
 	if (mode & ACL_USAGE)
 	{
-		if (has_privs_of_role(roleid, role_oid))
+		if (has_privs_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_SET)
@@ -5034,7 +5034,7 @@ initialize_acl(void)
 		 * of pg_auth_members (for roles_is_member_of()) pg_database (for
 		 * roles_is_member_of())
 		 */
-		CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
+		CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB,
 									  RoleMembershipCacheCallback,
 									  (Datum) 0);
 		CacheRegisterSyscacheCallback(AUTHOID,
@@ -5114,7 +5114,62 @@ roles_list_append(List *roles_list, bloom_filter **bf, Oid role)
 }
 
 /*
- * Get a list of roles that the specified roleid is a member of
+ * Append role memberships to the list of roles
+ */
+static void
+append_role_memberships(List *roles_list, enum RoleRecurseType type,
+						Oid admin_of, Oid *admin_role, Oid memberid,
+						Oid databaseId, bloom_filter *bf)
+{
+	CatCList   *memlist;
+	int			i;
+	Oid			targetDatabaseId;
+
+	if (!OidIsValid(databaseId))
+		targetDatabaseId = InvalidOid;
+	else
+		targetDatabaseId = MyDatabaseId;
+
+	memlist = SearchSysCacheList2(AUTHMEMMEMDBROLE,
+								  ObjectIdGetDatum(memberid),
+								  ObjectIdGetDatum(targetDatabaseId));
+	for (i = 0; i < memlist->n_members; i++)
+	{
+		HeapTuple	tup = &memlist->members[i]->tuple;
+		Form_pg_auth_members form = (Form_pg_auth_members) GETSTRUCT(tup);
+		Oid			otherid = form->roleid;
+		Oid			db_id = form->dbid;
+
+		/*
+		 * While otherid==InvalidOid shouldn't appear in the catalog, the
+		 * OidIsValid() avoids crashing if that arises.
+		 */
+		if (otherid == admin_of && form->admin_option &&
+			db_id == databaseId &&
+			OidIsValid(admin_of) && !OidIsValid(*admin_role))
+			*admin_role = memberid;
+
+		/* If we're supposed to ignore non-heritable grants, do so. */
+		if (type == ROLERECURSE_PRIVS && db_id == databaseId && !form->inherit_option)
+			continue;
+
+		/* If we're supposed to ignore non-SET grants, do so. */
+		if (type == ROLERECURSE_SETROLE && db_id == databaseId && !form->set_option)
+			continue;
+
+		/*
+		 * 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 = roles_list_append(roles_list, &bf, otherid);
+	}
+	ReleaseSysCacheList(memlist);
+}
+
+/*
+ * Get a list of roles that the specified roleid is a member of, possibly
+ * scoped to specified database with databaseId.
  *
  * Type ROLERECURSE_MEMBERS recurses through all grants; ROLERECURSE_PRIVS
  * recurses only through inheritable grants; and ROLERECURSE_SETROLE recurses
@@ -5134,7 +5189,7 @@ roles_list_append(List *roles_list, bloom_filter **bf, Oid role)
  */
 static List *
 roles_is_member_of(Oid roleid, enum RoleRecurseType type,
-				   Oid admin_of, Oid *admin_role)
+				   Oid admin_of, Oid *admin_role, Oid databaseId)
 {
 	Oid			dba;
 	List	   *roles_list;
@@ -5157,7 +5212,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 	 * ROLE_PG_READ_ALL_SETTINGS for a physical walsender SHOW command.  In
 	 * that case, no role gets pg_database_owner.
 	 */
-	if (!OidIsValid(MyDatabaseId))
+	if (!OidIsValid(MyDatabaseId))	/* XXX or databaseId? */
 		dba = InvalidOid;
 	else
 	{
@@ -5186,42 +5241,18 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 	foreach(l, roles_list)
 	{
 		Oid			memberid = lfirst_oid(l);
-		CatCList   *memlist;
-		int			i;
 
 		/* 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;
-			Form_pg_auth_members form = (Form_pg_auth_members) GETSTRUCT(tup);
-			Oid			otherid = form->roleid;
-
-			/*
-			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
-			 */
-			if (otherid == admin_of && form->admin_option &&
-				OidIsValid(admin_of) && !OidIsValid(*admin_role))
-				*admin_role = memberid;
-
-			/* If we're supposed to ignore non-heritable grants, do so. */
-			if (type == ROLERECURSE_PRIVS && !form->inherit_option)
-				continue;
+		append_role_memberships(roles_list, type, admin_of, admin_role,
+								memberid, InvalidOid, bf);
 
-			/* If we're supposed to ignore non-SET grants, do so. */
-			if (type == ROLERECURSE_SETROLE && !form->set_option)
-				continue;
-
-			/*
-			 * 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 = roles_list_append(roles_list, &bf, otherid);
-		}
-		ReleaseSysCacheList(memlist);
+		/*
+		 * Find roles that memberid is directly a member of in the current
+		 * database
+		 */
+		if (OidIsValid(databaseId))
+			append_role_memberships(roles_list, type, admin_of, admin_role,
+									memberid, databaseId, bf);
 
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
@@ -5257,7 +5288,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 
 /*
- * Does member have the privileges of role (directly or indirectly)?
+ * Does member have the privileges of role (directly or indirectly) in
+ * specified database or cluster-wise?
  *
  * This is defined not to recurse through grants that are not inherited,
  * and only inherited grants confer the associated privileges automatically.
@@ -5265,7 +5297,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
  * See also member_can_set_role, below.
  */
 bool
-has_privs_of_role(Oid member, Oid role)
+has_privs_of_role(Oid member, Oid role, Oid database)
 {
 	/* Fast path for simple case */
 	if (member == role)
@@ -5280,10 +5312,21 @@ 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, database),
 						   role);
 }
 
+
+/*
+ * Does member have the privileges of role *cluster-wide*?
+ *
+ */
+bool
+has_cluster_privs_of_role(Oid member, Oid role)
+{
+	return has_privs_of_role(member, role, InvalidOid);
+}
+
 /*
  * Can member use SET ROLE to this role?
  *
@@ -5314,7 +5357,7 @@ member_can_set_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_SETROLE,
-											  InvalidOid, NULL),
+											  InvalidOid, NULL, MyDatabaseId),
 						   role);
 }
 
@@ -5360,7 +5403,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);
 }
 
@@ -5384,7 +5427,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);
 }
 
@@ -5395,7 +5438,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)
 {
 	Oid			admin_role;
 
@@ -5406,7 +5449,7 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role, databaseId);
 	return OidIsValid(admin_role);
 }
 
@@ -5420,7 +5463,7 @@ is_admin_of_role(Oid member, Oid role)
  * over more indirect inheritance.
  */
 Oid
-select_best_admin(Oid member, Oid role)
+select_best_admin(Oid member, Oid role, Oid databaseId)
 {
 	Oid			admin_role;
 
@@ -5428,7 +5471,7 @@ select_best_admin(Oid member, Oid role)
 	if (member == role)
 		return InvalidOid;
 
-	(void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+	(void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role, databaseId);
 	return admin_role;
 }
 
@@ -5504,7 +5547,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/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index e63e99c141..2f31ecb0c4 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -130,7 +130,7 @@ calculate_database_size(Oid dbOid)
 	 */
 	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(), ACL_CONNECT);
 	if (aclresult != ACLCHECK_OK &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		aclcheck_error(aclresult, OBJECT_DATABASE,
 					   get_database_name(dbOid));
@@ -214,7 +214,7 @@ calculate_tablespace_size(Oid tblspcOid)
 	 * it is default for current database.
 	 */
 	if (tblspcOid != MyDatabaseTableSpace &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
 		if (aclresult != ACLCHECK_OK)
diff --git a/src/backend/utils/adt/genfile.c b/src/backend/utils/adt/genfile.c
index 24b95c32b7..5f2a4b5997 100644
--- a/src/backend/utils/adt/genfile.c
+++ b/src/backend/utils/adt/genfile.c
@@ -63,7 +63,7 @@ convert_and_check_filename(text *arg)
 	 * access any files on the server as the PG user, so no need to do any
 	 * further checks here.
 	 */
-	if (has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+	if (has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 		return filename;
 
 	/*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9c23ac7c8c..804d07712d 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -34,7 +34,7 @@
 
 #define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))
 
-#define HAS_PGSTAT_PERMISSIONS(role)	 (has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) || has_privs_of_role(GetUserId(), role))
+#define HAS_PGSTAT_PERMISSIONS(role)	 (has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) || has_cluster_privs_of_role(GetUserId(), role))
 
 #define PG_STAT_GET_RELENTRY_INT64(stat)						\
 Datum															\
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 10276aa1db..e8b601301c 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1223,7 +1223,7 @@ IndexScanOK(CatCache *cache)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHMEMMEMROLE:
+		case AUTHMEMMEMDBROLE:
 		case DATABASEOID:
 
 			/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 5b6b7b809c..69ab4d5f93 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4208,7 +4208,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/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d..85f5dec665 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -922,7 +922,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 					 errmsg("remaining connection slots are reserved for roles with the %s attribute",
 							"SUPERUSER")));
 
-		if (!has_privs_of_role(GetUserId(), ROLE_PG_USE_RESERVED_CONNECTIONS))
+		if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_USE_RESERVED_CONNECTIONS))
 			ereport(FATAL,
 					(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
 					 errmsg("remaining connection slots are reserved for roles with privileges of the \"%s\" role",
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index 9c9edd3d2f..2a845564f1 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -581,7 +581,7 @@ bool
 ConfigOptionIsVisible(struct config_generic *conf)
 {
 	if ((conf->flags & GUC_SUPERUSER_ONLY) &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
 		return false;
 	else
 		return true;
@@ -784,7 +784,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values)
 	 * insufficiently-privileged users.
 	 */
 	if (conf->source == PGC_S_FILE &&
-		has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
+		has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
 	{
 		values[14] = conf->sourcefile;
 		snprintf(buffer, sizeof(buffer), "%d", conf->sourceline);
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb295..67ebbf24f0 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -58,7 +58,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 dumpRoleGUCPrivs(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -594,7 +594,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 
 			/* Dump role GUC privileges */
 			if (server_version >= 150000 && !skip_acls)
@@ -955,7 +955,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQExpBuffer optbuf = createPQExpBuffer();
@@ -995,7 +995,8 @@ dumpRoleMembership(PGconn *conn)
 					  "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,4", role_catalog, role_catalog, role_catalog);
+					  " AND a.dbid = %s "
+					  "ORDER BY 1,2,4", role_catalog, role_catalog, role_catalog, databaseId);
 	res = executeQuery(conn, buf->data);
 	i_inherit_option = PQfnumber(res, "inherit_option");
 	i_set_option = PQfnumber(res, "set_option");
@@ -1110,6 +1111,8 @@ dumpRoleMembership(PGconn *conn)
 				resetPQExpBuffer(optbuf);
 				fprintf(OPF, "GRANT %s", fmtId(role));
 				fprintf(OPF, " TO %s", fmtId(member));
+				if (strcmp(databaseId, "0") != 0)
+					fprintf(OPF, " IN CURRENT DATABASE");
 				if (*admin_option == 't')
 					appendPQExpBufferStr(optbuf, "ADMIN OPTION");
 				if (dump_grant_options)
@@ -1504,7 +1507,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 AND datconnlimit != -2 "
 					   "ORDER BY (datname <> 'template1'), datname");
@@ -1515,6 +1518,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;
 
@@ -1555,6 +1559,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 16.0 or later */
+		if (server_version >= 160000)
+			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 5f4b30756c..aa1b80baca 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -36,6 +36,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
 	bool		admin_option;	/* granted with admin option? */
 	bool		inherit_option; /* exercise privileges without SET ROLE? */
 	bool		set_option;		/* use SET ROLE to the target role? */
+	Oid			dbid BKI_LOOKUP_OPT(pg_database);	/* ID of a database this
+													 * mapping is effective in */
 } FormData_pg_auth_members;
 
 /* ----------------
@@ -46,11 +48,11 @@ 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_oid_index, 6303, AuthMemOidIndexId, pg_auth_members, btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, pg_auth_members, btree(roleid oid_ops, member oid_ops, grantor oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, pg_auth_members, btree(member oid_ops, roleid oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_db_index, 2694, AuthMemRoleMemDbIndexId, pg_auth_members, btree(roleid oid_ops, member oid_ops, grantor oid_ops, dbid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_db_role_index, 2695, AuthMemMemRoleDbIndexId, pg_auth_members, btree(member oid_ops, dbid oid_ops, roleid oid_ops, grantor oid_ops));
 DECLARE_INDEX(pg_auth_members_grantor_index, 6302, AuthMemGrantorIndexId, pg_auth_members, btree(grantor oid_ops));
 
-MAKE_SYSCACHE(AUTHMEMROLEMEM, pg_auth_members_role_member_index, 8);
-MAKE_SYSCACHE(AUTHMEMMEMROLE, pg_auth_members_member_role_index, 8);
+MAKE_SYSCACHE(AUTHMEMROLEMEMDB, pg_auth_members_role_member_db_index, 8);
+MAKE_SYSCACHE(AUTHMEMMEMDBROLE, pg_auth_members_member_db_role_index, 8);
 
 #endif							/* PG_AUTH_MEMBERS_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 97dcb93791..5444b74000 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -35,6 +35,7 @@ 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);
 
 extern bool check_createrole_self_grant(char **newval, void **extra,
 										GucSource source);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e62ce1b753..29e0040149 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2557,6 +2557,9 @@ 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 means
+								 * global, "" for the current database,
+								 * otherwise a named database */
 	bool		is_grant;		/* true = GRANT, false = REVOKE */
 	List	   *opt;			/* options e.g. WITH GRANT 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 731d84b2a9..d6e7259e8a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,13 +209,14 @@ extern AclMode aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 					   AclMode mask, AclMaskHow how);
 extern int	aclmembers(const Acl *acl, Oid **roleids);
 
-extern bool has_privs_of_role(Oid member, Oid role);
+extern bool has_privs_of_role(Oid member, Oid role, Oid database);
+extern bool has_cluster_privs_of_role(Oid member, Oid role);
 extern bool member_can_set_role(Oid member, Oid role);
 extern void check_can_set_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 Oid	select_best_admin(Oid member, Oid role);
+extern bool is_admin_of_role(Oid member, Oid role, Oid databaseId);
+extern Oid	select_best_admin(Oid member, Oid role, Oid databaseId);
 extern Oid	get_role_oid(const char *rolname, bool missing_ok);
 extern Oid	get_role_oid_or_public(const char *rolname);
 extern Oid	get_rolespec_oid(const RoleSpec *role, bool missing_ok);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..79fb69059b 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}
diff --git a/src/test/regress/expected/privs_in_db.out b/src/test/regress/expected/privs_in_db.out
new file mode 100644
index 0000000000..ff3c2f589c
--- /dev/null
+++ b/src/test/regress/expected/privs_in_db.out
@@ -0,0 +1,767 @@
+--
+-- Tests for database-specific role memberships.
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP ROLE IF EXISTS regress_priv_group1;
+DROP ROLE IF EXISTS regress_priv_group2;
+DROP DATABASE IF EXISTS db_4;
+DROP DATABASE IF EXISTS db_3;
+DROP DATABASE IF EXISTS db_2;
+DROP DATABASE IF EXISTS db_1;
+DROP ROLE IF EXISTS role_granted;
+DROP ROLE IF EXISTS role_read_34;
+DROP ROLE IF EXISTS role_inherited_3;
+DROP ROLE IF EXISTS role_inherited_34;
+DROP ROLE IF EXISTS role_read_0;
+DROP ROLE IF EXISTS role_read_12;
+DROP ROLE IF EXISTS role_read_12_noinherit;
+DROP ROLE IF EXISTS role_read_all_noinherit;
+DROP ROLE IF EXISTS role_read_all_with_admin;
+DROP DATABASE IF EXISTS db_0;
+DROP ROLE IF EXISTS role_admin;
+RESET client_min_messages;
+-- test proper begins here
+CREATE ROLE role_admin LOGIN CREATEROLE CREATEDB;
+GRANT pg_read_all_data TO role_admin WITH ADMIN OPTION;
+GRANT pg_read_all_stats TO role_admin WITH ADMIN OPTION;
+GRANT pg_maintain TO role_admin WITH ADMIN OPTION;
+-- Populate test databases.
+CREATE DATABASE db_0 OWNER role_admin;
+\connect db_0
+CREATE TABLE data AS SELECT generate_series(1, 3);
+CREATE VIEW role_memberships
+ AS
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  CASE WHEN g.rolsuper THEN 'superuser' ELSE g.rolname END as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+JOIN pg_roles r ON r.oid = a.roleid
+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, 3
+;
+CREATE DATABASE db_1 TEMPLATE db_0 OWNER role_admin;
+CREATE DATABASE db_2 TEMPLATE db_1 OWNER role_admin;
+CREATE DATABASE db_3 TEMPLATE db_1 OWNER role_admin;
+CREATE DATABASE db_4 TEMPLATE db_1 OWNER role_admin;
+SET SESSION AUTHORIZATION role_admin;
+-- Read all cluster-wide with admin option
+CREATE ROLE role_read_all_with_admin ROLE role_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 ROLE role_admin;
+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 ROLE role_admin;
+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 ROLE role_admin;
+GRANT role_read_34 TO role_inherited_34;
+-- Inherits read all in database 3
+CREATE ROLE role_inherited_3 ROLE role_admin;
+GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3;
+-- No inherit
+CREATE ROLE role_read_all_noinherit NOINHERIT ROLE role_admin;
+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 ROLE role_admin;
+GRANT role_read_12 TO role_read_12_noinherit;
+-- Alternate syntax
+CREATE ROLE role_read_0;
+GRANT pg_read_all_data TO role_read_0, role_read_all_noinherit IN CURRENT DATABASE;
+-- Failure due to missing database
+GRANT pg_read_all_data TO role_read_0 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" has already been granted membership in role "pg_read_all_data" by role "role_admin"
+GRANT pg_read_all_data TO role_read_0 IN DATABASE db_0; -- notice
+NOTICE:  role "role_read_0" has already been granted membership in role "pg_read_all_data" in database "db_0" by role "role_admin"
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO role_read_0 IN DATABASE db_0 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO role_read_0 IN DATABASE db_0 WITH ADMIN OPTION; -- notice
+NOTICE:  role "role_read_0" has already been granted membership in role "pg_read_all_data" in database "db_0" by role "role_admin"
+GRANT pg_maintain TO role_read_12 IN DATABASE db_2;
+-- Cluster-wide role
+GRANT pg_read_all_stats TO role_read_0;
+GRANT pg_read_all_stats TO role_read_34 IN DATABASE db_3;  -- makes no sense XXX
+-- Check membership table
+TABLE role_memberships;
+           role           |          member          |  grantor   | admin_option | datname 
+--------------------------+--------------------------+------------+--------------+---------
+ pg_maintain              | role_admin               | superuser  | t            | 
+ pg_maintain              | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_admin               | superuser  | t            | 
+ pg_read_all_data         | role_read_0              | role_admin | t            | db_0
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_stats        | role_admin               | superuser  | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin | f            | db_3
+ role_inherited_3         | role_admin               | role_admin | f            | 
+ role_inherited_3         | role_admin               | superuser  | t            | 
+ role_inherited_34        | role_admin               | role_admin | f            | 
+ role_inherited_34        | role_admin               | superuser  | t            | 
+ role_read_0              | role_admin               | superuser  | t            | 
+ role_read_12             | role_admin               | role_admin | f            | 
+ role_read_12             | role_admin               | superuser  | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | superuser  | t            | 
+ role_read_34             | role_admin               | role_admin | f            | 
+ role_read_34             | role_admin               | superuser  | t            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_admin               | role_admin | f            | 
+ role_read_all_with_admin | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(32 rows)
+
+-- Test membership privileges (db_1)
+\connect db_1
+SET SESSION AUTHORIZATION role_admin;
+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;
+VACUUM data; -- error
+WARNING:  permission denied to vacuum "data", skipping it
+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 SESSION AUTHORIZATION role_admin;
+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;
+VACUUM data; -- success
+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 SESSION AUTHORIZATION role_admin;
+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 SESSION AUTHORIZATION role_admin;
+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 db_0
+-- Test cluster-wide role
+SET SESSION AUTHORIZATION role_read_0;
+SELECT query FROM pg_stat_activity WHERE datname = 'db_0';
+                           query                            
+------------------------------------------------------------
+ SELECT query FROM pg_stat_activity WHERE datname = 'db_0';
+(1 row)
+
+SET SESSION AUTHORIZATION role_read_12;
+SELECT query FROM pg_stat_activity WHERE datname = 'db_0';
+          query           
+--------------------------
+ <insufficient privilege>
+(1 row)
+
+\connect db_3
+SET SESSION AUTHORIZATION role_read_34;
+SELECT application_name, query FROM pg_stat_activity WHERE datname = 'db_3';
+    application_name    |          query           
+------------------------+--------------------------
+ pg_regress/privs_in_db | <insufficient privilege>
+(1 row)
+
+\connect db_0
+SET SESSION AUTHORIZATION role_admin;
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- silent
+TABLE role_memberships;
+           role           |          member          |  grantor   | admin_option | datname 
+--------------------------+--------------------------+------------+--------------+---------
+ pg_maintain              | role_admin               | superuser  | t            | 
+ pg_maintain              | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_admin               | superuser  | t            | 
+ pg_read_all_data         | role_read_0              | role_admin | f            | db_0
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_stats        | role_admin               | superuser  | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin | f            | db_3
+ role_inherited_3         | role_admin               | role_admin | f            | 
+ role_inherited_3         | role_admin               | superuser  | t            | 
+ role_inherited_34        | role_admin               | role_admin | f            | 
+ role_inherited_34        | role_admin               | superuser  | t            | 
+ role_read_0              | role_admin               | superuser  | t            | 
+ role_read_12             | role_admin               | role_admin | f            | 
+ role_read_12             | role_admin               | superuser  | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | superuser  | t            | 
+ role_read_34             | role_admin               | role_admin | f            | 
+ role_read_34             | role_admin               | superuser  | t            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_admin               | role_admin | f            | 
+ role_read_all_with_admin | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(32 rows)
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- success
+REVOKE pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- warning
+WARNING:  role "role_read_0" has not been granted membership in role "pg_read_all_data" by role "role_admin"
+TABLE role_memberships;
+           role           |          member          |  grantor   | admin_option | datname 
+--------------------------+--------------------------+------------+--------------+---------
+ pg_maintain              | role_admin               | superuser  | t            | 
+ pg_maintain              | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_admin               | superuser  | t            | 
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_stats        | role_admin               | superuser  | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin | f            | db_3
+ role_inherited_3         | role_admin               | role_admin | f            | 
+ role_inherited_3         | role_admin               | superuser  | t            | 
+ role_inherited_34        | role_admin               | role_admin | f            | 
+ role_inherited_34        | role_admin               | superuser  | t            | 
+ role_read_0              | role_admin               | superuser  | t            | 
+ role_read_12             | role_admin               | role_admin | f            | 
+ role_read_12             | role_admin               | superuser  | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | superuser  | t            | 
+ role_read_34             | role_admin               | role_admin | f            | 
+ role_read_34             | role_admin               | superuser  | t            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_admin               | role_admin | f            | 
+ role_read_all_with_admin | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(31 rows)
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM role_read_12; -- warning
+TABLE role_memberships;
+           role           |          member          |  grantor   | admin_option | datname 
+--------------------------+--------------------------+------------+--------------+---------
+ pg_maintain              | role_admin               | superuser  | t            | 
+ pg_maintain              | role_read_12             | role_admin | f            | db_2
+ pg_read_all_data         | role_admin               | superuser  | t            | 
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin | t            | 
+ pg_read_all_stats        | role_admin               | superuser  | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin | f            | db_3
+ role_inherited_3         | role_admin               | role_admin | f            | 
+ role_inherited_3         | role_admin               | superuser  | t            | 
+ role_inherited_34        | role_admin               | role_admin | f            | 
+ role_inherited_34        | role_admin               | superuser  | t            | 
+ role_read_0              | role_admin               | superuser  | t            | 
+ role_read_12             | role_admin               | role_admin | f            | 
+ role_read_12             | role_admin               | superuser  | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin | f            | 
+ role_read_12_noinherit   | role_admin               | superuser  | t            | 
+ role_read_34             | role_admin               | role_admin | f            | 
+ role_read_34             | role_admin               | superuser  | t            | 
+ role_read_34             | role_inherited_3         | role_admin | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin | f            | 
+ role_read_all_noinherit  | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_admin               | role_admin | f            | 
+ role_read_all_with_admin | role_admin               | superuser  | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin | f            | 
+(30 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:  permission denied to grant role "role_read_34"
+DETAIL:  Only roles with the ADMIN option on role "role_read_34" may grant this role.
+TABLE role_memberships;
+           role           |          member          |         grantor          | admin_option | datname 
+--------------------------+--------------------------+--------------------------+--------------+---------
+ pg_maintain              | role_admin               | superuser                | t            | 
+ pg_maintain              | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_admin               | superuser                | t            | 
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_0
+ 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            | 
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ pg_read_all_stats        | role_admin               | superuser                | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin               | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin               | f            | db_3
+ role_granted             | role_admin               | superuser                | t            | 
+ role_inherited_3         | role_admin               | role_admin               | f            | 
+ role_inherited_3         | role_admin               | superuser                | t            | 
+ role_inherited_34        | role_admin               | role_admin               | f            | 
+ role_inherited_34        | role_admin               | superuser                | t            | 
+ role_read_0              | role_admin               | superuser                | t            | 
+ role_read_12             | role_admin               | role_admin               | f            | 
+ role_read_12             | role_admin               | superuser                | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | superuser                | t            | 
+ role_read_34             | role_admin               | role_admin               | f            | 
+ role_read_34             | role_admin               | superuser                | t            | 
+ role_read_34             | role_inherited_3         | role_admin               | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_admin               | role_admin               | f            | 
+ role_read_all_with_admin | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(34 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:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+\connect db_3
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+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" has already been granted membership in role "pg_read_all_data" in database "db_3" by role "role_read_34"
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+\connect db_4
+SET SESSION AUTHORIZATION role_read_34;
+GRANT pg_read_all_data TO role_granted; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+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:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- notice
+NOTICE:  role "role_granted" has already been granted membership in role "pg_read_all_data" in database "db_4" by role "role_read_34"
+\connect db_0
+SET SESSION AUTHORIZATION role_admin;
+TABLE role_memberships;
+           role           |          member          |         grantor          | admin_option | datname 
+--------------------------+--------------------------+--------------------------+--------------+---------
+ pg_maintain              | role_admin               | superuser                | t            | 
+ pg_maintain              | role_read_12             | role_admin               | f            | db_2
+ pg_read_all_data         | role_admin               | superuser                | t            | 
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_0
+ 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            | 
+ 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            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ pg_read_all_stats        | role_admin               | superuser                | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin               | f            | 
+ pg_read_all_stats        | role_read_34             | role_admin               | f            | db_3
+ role_granted             | role_admin               | superuser                | t            | 
+ role_inherited_3         | role_admin               | role_admin               | f            | 
+ role_inherited_3         | role_admin               | superuser                | t            | 
+ role_inherited_34        | role_admin               | role_admin               | f            | 
+ role_inherited_34        | role_admin               | superuser                | t            | 
+ role_read_0              | role_admin               | superuser                | t            | 
+ role_read_12             | role_admin               | role_admin               | f            | 
+ role_read_12             | role_admin               | superuser                | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | superuser                | t            | 
+ role_read_34             | role_admin               | role_admin               | f            | 
+ role_read_34             | role_admin               | superuser                | t            | 
+ role_read_34             | role_inherited_3         | role_admin               | f            | db_3
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_admin               | role_admin               | f            | 
+ role_read_all_with_admin | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(36 rows)
+
+-- Should clean up the membership table when dropping a database
+DROP DATABASE db_4;
+DROP DATABASE db_3;
+DROP DATABASE db_2;
+DROP DATABASE db_1;
+TABLE role_memberships;
+           role           |          member          |         grantor          | admin_option | datname 
+--------------------------+--------------------------+--------------------------+--------------+---------
+ pg_maintain              | role_admin               | superuser                | t            | 
+ pg_read_all_data         | role_admin               | superuser                | t            | 
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | db_0
+ pg_read_all_data         | role_granted             | role_read_all_with_admin | f            | 
+ pg_read_all_data         | role_read_all_noinherit  | role_admin               | f            | db_0
+ pg_read_all_data         | role_read_all_with_admin | role_admin               | t            | 
+ pg_read_all_stats        | role_admin               | superuser                | t            | 
+ pg_read_all_stats        | role_read_0              | role_admin               | f            | 
+ role_granted             | role_admin               | superuser                | t            | 
+ role_inherited_3         | role_admin               | role_admin               | f            | 
+ role_inherited_3         | role_admin               | superuser                | t            | 
+ role_inherited_34        | role_admin               | role_admin               | f            | 
+ role_inherited_34        | role_admin               | superuser                | t            | 
+ role_read_0              | role_admin               | superuser                | t            | 
+ role_read_12             | role_admin               | role_admin               | f            | 
+ role_read_12             | role_admin               | superuser                | t            | 
+ role_read_12             | role_read_12_noinherit   | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | role_admin               | f            | 
+ role_read_12_noinherit   | role_admin               | superuser                | t            | 
+ role_read_34             | role_admin               | role_admin               | f            | 
+ role_read_34             | role_admin               | superuser                | t            | 
+ role_read_34             | role_inherited_34        | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | role_admin               | f            | 
+ role_read_all_noinherit  | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_admin               | role_admin               | f            | 
+ role_read_all_with_admin | role_admin               | superuser                | t            | 
+ role_read_all_with_admin | role_read_all_noinherit  | role_admin               | f            | 
+(27 rows)
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE role_granted;  -- dependency of 'role_read_34'
+DROP ROLE role_read_34;
+DROP ROLE role_inherited_3;
+DROP ROLE role_inherited_34;
+DROP ROLE role_read_0;
+DROP ROLE role_read_12;
+DROP ROLE role_read_12_noinherit;
+DROP ROLE role_read_all_noinherit;
+DROP ROLE role_read_all_with_admin;
+RESET SESSION AUTHORIZATION;
+DROP OWNED BY role_admin CASCADE;
+TABLE role_memberships;
+       role        |   member   |  grantor  | admin_option | datname 
+-------------------+------------+-----------+--------------+---------
+ pg_maintain       | role_admin | superuser | t            | 
+ pg_read_all_data  | role_admin | superuser | t            | 
+ pg_read_all_stats | role_admin | superuser | t            | 
+(3 rows)
+
+\connect template1
+DROP DATABASE db_0;
+DROP ROLE role_admin;
+SELECT datname FROM pg_database WHERE datname LIKE 'db_%';
+ datname 
+---------
+(0 rows)
+
+SELECT rolname FROM pg_roles WHERE rolname LIKE 'role_%';
+ rolname 
+---------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4f38104ba0..35d5f67059 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -92,7 +92,7 @@ test: write_parallel
 test: vacuum_parallel
 
 # no relation related tests can be put in this group
-test: publication subscription
+test: publication subscription privs_in_db
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/privs_in_db.sql b/src/test/regress/sql/privs_in_db.sql
new file mode 100644
index 0000000000..a4ec581c8a
--- /dev/null
+++ b/src/test/regress/sql/privs_in_db.sql
@@ -0,0 +1,370 @@
+--
+-- Tests for database-specific role memberships.
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP ROLE IF EXISTS regress_priv_group1;
+DROP ROLE IF EXISTS regress_priv_group2;
+
+DROP DATABASE IF EXISTS db_4;
+DROP DATABASE IF EXISTS db_3;
+DROP DATABASE IF EXISTS db_2;
+DROP DATABASE IF EXISTS db_1;
+DROP ROLE IF EXISTS role_granted;
+DROP ROLE IF EXISTS role_read_34;
+DROP ROLE IF EXISTS role_inherited_3;
+DROP ROLE IF EXISTS role_inherited_34;
+DROP ROLE IF EXISTS role_read_0;
+DROP ROLE IF EXISTS role_read_12;
+DROP ROLE IF EXISTS role_read_12_noinherit;
+DROP ROLE IF EXISTS role_read_all_noinherit;
+DROP ROLE IF EXISTS role_read_all_with_admin;
+DROP DATABASE IF EXISTS db_0;
+DROP ROLE IF EXISTS role_admin;
+
+RESET client_min_messages;
+
+-- test proper begins here
+
+CREATE ROLE role_admin LOGIN CREATEROLE CREATEDB;
+GRANT pg_read_all_data TO role_admin WITH ADMIN OPTION;
+GRANT pg_read_all_stats TO role_admin WITH ADMIN OPTION;
+GRANT pg_maintain TO role_admin WITH ADMIN OPTION;
+
+-- Populate test databases.
+CREATE DATABASE db_0 OWNER role_admin;
+
+\connect db_0
+CREATE TABLE data AS SELECT generate_series(1, 3);
+
+CREATE VIEW role_memberships
+ AS
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  CASE WHEN g.rolsuper THEN 'superuser' ELSE g.rolname END as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+JOIN pg_roles r ON r.oid = a.roleid
+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, 3
+;
+
+CREATE DATABASE db_1 TEMPLATE db_0 OWNER role_admin;
+CREATE DATABASE db_2 TEMPLATE db_1 OWNER role_admin;
+CREATE DATABASE db_3 TEMPLATE db_1 OWNER role_admin;
+CREATE DATABASE db_4 TEMPLATE db_1 OWNER role_admin;
+
+SET SESSION AUTHORIZATION role_admin;
+
+-- Read all cluster-wide with admin option
+CREATE ROLE role_read_all_with_admin ROLE role_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 ROLE role_admin;
+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 ROLE role_admin;
+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 ROLE role_admin;
+GRANT role_read_34 TO role_inherited_34;
+
+-- Inherits read all in database 3
+CREATE ROLE role_inherited_3 ROLE role_admin;
+GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3;
+
+-- No inherit
+CREATE ROLE role_read_all_noinherit NOINHERIT ROLE role_admin;
+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 ROLE role_admin;
+GRANT role_read_12 TO role_read_12_noinherit;
+
+-- Alternate syntax
+CREATE ROLE role_read_0;
+GRANT pg_read_all_data TO role_read_0, role_read_all_noinherit IN CURRENT DATABASE;
+
+-- Failure due to missing database
+GRANT pg_read_all_data TO role_read_0 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_0 IN DATABASE db_0; -- notice
+
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO role_read_0 IN DATABASE db_0 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO role_read_0 IN DATABASE db_0 WITH ADMIN OPTION; -- notice
+
+GRANT pg_maintain TO role_read_12 IN DATABASE db_2;
+
+-- Cluster-wide role
+GRANT pg_read_all_stats TO role_read_0;
+GRANT pg_read_all_stats TO role_read_34 IN DATABASE db_3;  -- makes no sense XXX
+
+-- Check membership table
+TABLE role_memberships;
+
+-- Test membership privileges (db_1)
+\connect db_1
+SET SESSION AUTHORIZATION role_admin;
+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;
+VACUUM data; -- error
+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 SESSION AUTHORIZATION role_admin;
+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;
+VACUUM data; -- success
+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 SESSION AUTHORIZATION role_admin;
+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 SESSION AUTHORIZATION role_admin;
+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 db_0
+
+-- Test cluster-wide role
+SET SESSION AUTHORIZATION role_read_0;
+SELECT query FROM pg_stat_activity WHERE datname = 'db_0';
+SET SESSION AUTHORIZATION role_read_12;
+SELECT query FROM pg_stat_activity WHERE datname = 'db_0';
+
+\connect db_3
+SET SESSION AUTHORIZATION role_read_34;
+SELECT application_name, query FROM pg_stat_activity WHERE datname = 'db_3';
+
+\connect db_0
+SET SESSION AUTHORIZATION role_admin;
+
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- silent
+TABLE role_memberships;
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- success
+REVOKE pg_read_all_data FROM role_read_0 IN DATABASE db_0; -- warning
+TABLE role_memberships;
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM role_read_12; -- warning
+TABLE role_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
+TABLE role_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 db_0
+SET SESSION AUTHORIZATION role_admin;
+TABLE role_memberships;
+
+-- Should clean up the membership table when dropping a database
+DROP DATABASE db_4;
+DROP DATABASE db_3;
+DROP DATABASE db_2;
+DROP DATABASE db_1;
+TABLE role_memberships;
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE role_granted;  -- dependency of 'role_read_34'
+DROP ROLE role_read_34;
+DROP ROLE role_inherited_3;
+DROP ROLE role_inherited_34;
+DROP ROLE role_read_0;
+DROP ROLE role_read_12;
+DROP ROLE role_read_12_noinherit;
+DROP ROLE role_read_all_noinherit;
+DROP ROLE role_read_all_with_admin;
+
+RESET SESSION AUTHORIZATION;
+DROP OWNED BY role_admin CASCADE;
+TABLE role_memberships;
+\connect template1
+DROP DATABASE db_0;
+DROP ROLE role_admin;
+SELECT datname FROM pg_database WHERE datname LIKE 'db_%';
+SELECT rolname FROM pg_roles WHERE rolname LIKE 'role_%';

base-commit: 62ddf7ee9a399e0b9624412fc482ed7365e38958
-- 
2.39.5

#32Denis Laxalde
denis.laxalde@dalibo.com
In reply to: Denis Laxalde (#31)
Re: Proposal: allow database-specific role memberships

Denis Laxalde a écrit :

Michael Paquier a écrit :

On Wed, Sep 07, 2022 at 12:50:32PM +0500, Ibrar Ahmed wrote:

The patch requires a rebase, please do that.

Hunk #5 succeeded at 454 (offset 28 lines). 1 out of 5 hunks FAILED
-- saving rejects to file doc/src/sgml/ref/grant.sgml.rej

There has been no updates on this thread for one month, so this has
been switched as RwF.

I took the liberty to rebase this (old) patch, originally authored by
Kenaniah Cerny.

As the original commitfest entry,
https://commitfest.postgresql.org/36/3374/, was "stalled", I created a
new one at https://commitfest.postgresql.org/50/5284/; hoping this is okay.

Show quoted text

This is about adding a "IN DATABASE <datname>" clause to GRANT and
REVOKE commands allowing to control role membership in a database scope,
rather that cluster-wise. This could be interesting in combination with
predefined roles, e.g.:

GRANT pg_read_all_data TO bob IN DATABASE app;
GRANT pg_maintain TO dba IN DATABASE metrics;

without having to grant too many privileges when a user is supposed to
only operate on some databases.

The logic of the original patch (as of its version 11) is preserved. One
noticeable change concerns tests: they got moved in src/test/regress
(there were in 'unsafe_tests'), with proper cleanup, and now avoid using
superuser as well as modifying templates.

Is this a feature that's still interesting? (Feedbacks, from 2022, in
the thread were a bit mixed.)

Personally, I have a few concerns regarding the feature and its
implementation:

- The IN DATABASE clause does not make much sense for some roles, like
pg_read_all_stats (the implementation does not guard against this).

- An 'IN SCHEMA' clause might be a natural supplementary feature.
However, the current implementation relying on a new 'dbid' column added
in pg_auth_members catalog might not fit well in that case.

#33Denis Laxalde
denis.laxalde@dalibo.com
In reply to: Denis Laxalde (#32)
1 attachment(s)
Re: Proposal: allow database-specific role memberships

Denis Laxalde a écrit :

Denis Laxalde a écrit :

Michael Paquier a écrit :

On Wed, Sep 07, 2022 at 12:50:32PM +0500, Ibrar Ahmed wrote:

The patch requires a rebase, please do that.

Hunk #5 succeeded at 454 (offset 28 lines). 1 out of 5 hunks FAILED
-- saving rejects to file doc/src/sgml/ref/grant.sgml.rej

There has been no updates on this thread for one month, so this has
been switched as RwF.

I took the liberty to rebase this (old) patch, originally authored by
Kenaniah Cerny.

As the original commitfest entry,
https://commitfest.postgresql.org/36/3374/, was "stalled", I created a
new one at https://commitfest.postgresql.org/50/5284/; hoping this is okay.

Attached a rebased version fixing test warnings raised in CI.

Show quoted text

This is about adding a "IN DATABASE <datname>" clause to GRANT and
REVOKE commands allowing to control role membership in a database scope,
rather that cluster-wise. This could be interesting in combination with
predefined roles, e.g.:

GRANT pg_read_all_data TO bob IN DATABASE app;
GRANT pg_maintain TO dba IN DATABASE metrics;

without having to grant too many privileges when a user is supposed to
only operate on some databases.

The logic of the original patch (as of its version 11) is preserved. One
noticeable change concerns tests: they got moved in src/test/regress
(there were in 'unsafe_tests'), with proper cleanup, and now avoid using
superuser as well as modifying templates.

Is this a feature that's still interesting? (Feedbacks, from 2022, in
the thread were a bit mixed.)

Personally, I have a few concerns regarding the feature and its
implementation:

- The IN DATABASE clause does not make much sense for some roles, like
pg_read_all_stats (the implementation does not guard against this).

- An 'IN SCHEMA' clause might be a natural supplementary feature.
However, the current implementation relying on a new 'dbid' column added
in pg_auth_members catalog might not fit well in that case.

Attachments:

Grant-revoke-role-membership-in-a-database-v13.patchtext/x-patch; charset=UTF-8; name=Grant-revoke-role-membership-in-a-database-v13.patchDownload
From 1d119e80857a77529dbec90897226250914945ac Mon Sep 17 00:00:00 2001
From: Denis Laxalde <denis.laxalde@dalibo.com>
Date: Fri, 5 Jul 2024 15:54:16 +0200
Subject: [PATCH] Grant/revoke role membership in a database

This adds a 'IN DATABASE <datname>' clause to GRANT/REVOKE commands
allowing to control role membership in a database scope. A major use
case is probably in combination with predefined roles, e.g.:

  GRANT pg_read_all_data TO bob IN DATABASE app;
  GRANT pg_maintain TO dba IN DATABASE metrics;

to avoid granting too many privileges when a user is supposed to only
operate on a subset of the cluster's databases.

The information about which database a role membership applies to is
stored in a new 'dbid' column of pg_auth_members catalog. When this
column is 0 (InvalidOid), the membership applies cluster-wise. Indexes
on this table are modified in order to include the new column to match
actual queries.

In roles_is_member_of(), the membership lookup is now performed first
for the database at stake, and then possibly repeated without a database
id for the cluster-wise case. The repetition of this operation is
factored out into a new append_role_memberships() helper function where
the previous logic now lives.

Function has_privs_of_role() now accepts a database id, as last
argument. For cases where a cluster-wise membership is sought, e.g. for
roles like 'pg_read_server_files', the new has_cluster_privs_of_role()
macro is used. The is_admin_of_role() function is modified similarly.

Original author: Kenaniah Cerny <kenaniah@gmail.com>.
---
 .../basebackup_to_shell/basebackup_to_shell.c |   2 +-
 contrib/file_fdw/file_fdw.c                   |   4 +-
 .../pg_stat_statements/pg_stat_statements.c   |   2 +-
 contrib/pgrowlocks/pgrowlocks.c               |   2 +-
 doc/src/sgml/catalogs.sgml                    |  20 +-
 doc/src/sgml/ref/grant.sgml                   |  46 +-
 doc/src/sgml/ref/revoke.sgml                  |   9 +
 doc/src/sgml/user-manag.sgml                  |  14 +
 src/backend/backup/basebackup_server.c        |   2 +-
 src/backend/catalog/aclchk.c                  |  14 +-
 src/backend/catalog/catalog.c                 |   4 +-
 src/backend/catalog/namespace.c               |   2 +-
 src/backend/catalog/objectaddress.c           |   2 +-
 src/backend/commands/alter.c                  |   6 +-
 src/backend/commands/copy.c                   |   6 +-
 src/backend/commands/dbcommands.c             |   6 +
 src/backend/commands/indexcmds.c              |   4 +-
 src/backend/commands/subscriptioncmds.c       |   2 +-
 src/backend/commands/user.c                   | 148 +++-
 src/backend/parser/gram.y                     |  33 +-
 src/backend/replication/walreceiver.c         |   2 +-
 src/backend/replication/walsender.c           |   2 +-
 src/backend/rewrite/rowsecurity.c             |   2 +-
 src/backend/storage/ipc/procarray.c           |   4 +-
 src/backend/storage/ipc/signalfuncs.c         |   6 +-
 src/backend/tcop/utility.c                    |   2 +-
 src/backend/utils/adt/acl.c                   | 147 ++--
 src/backend/utils/adt/dbsize.c                |   4 +-
 src/backend/utils/adt/genfile.c               |   2 +-
 src/backend/utils/adt/pgstatfuncs.c           |   2 +-
 src/backend/utils/cache/catcache.c            |   2 +-
 src/backend/utils/cache/relcache.c            |   2 +-
 src/backend/utils/init/postinit.c             |   2 +-
 src/backend/utils/misc/guc_funcs.c            |   4 +-
 src/bin/pg_dump/pg_dumpall.c                  |  18 +-
 src/include/catalog/pg_auth_members.h         |  10 +-
 src/include/commands/user.h                   |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/include/utils/acl.h                       |   7 +-
 src/test/regress/expected/oidjoins.out        |   1 +
 src/test/regress/expected/privs_in_db.out     | 767 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/privs_in_db.sql          | 370 +++++++++
 43 files changed, 1518 insertions(+), 172 deletions(-)
 create mode 100644 src/test/regress/expected/privs_in_db.out
 create mode 100644 src/test/regress/sql/privs_in_db.sql

diff --git a/contrib/basebackup_to_shell/basebackup_to_shell.c b/contrib/basebackup_to_shell/basebackup_to_shell.c
index fe419d90806..d35e2f61c27 100644
--- a/contrib/basebackup_to_shell/basebackup_to_shell.c
+++ b/contrib/basebackup_to_shell/basebackup_to_shell.c
@@ -104,7 +104,7 @@ shell_check_detail(char *target, char *target_detail)
 
 		StartTransactionCommand();
 		roleid = get_role_oid(shell_required_role, true);
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_cluster_privs_of_role(GetUserId(), roleid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to use basebackup_to_shell")));
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 043204c3e7e..d323f48d2ce 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -279,7 +279,7 @@ file_fdw_validator(PG_FUNCTION_ARGS)
 			 * otherwise there'd still be a security hole.
 			 */
 			if (strcmp(def->defname, "filename") == 0 &&
-				!has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+				!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to set the \"%s\" option of a file_fdw foreign table",
@@ -288,7 +288,7 @@ file_fdw_validator(PG_FUNCTION_ARGS)
 								   "pg_read_server_files")));
 
 			if (strcmp(def->defname, "program") == 0 &&
-				!has_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
+				!has_cluster_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to set the \"%s\" option of a file_fdw foreign table",
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 5765ef49b4c..26c3f8f992c 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -1644,7 +1644,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 	 * Superusers or roles with the privileges of pg_read_all_stats members
 	 * are allowed
 	 */
-	is_allowed_role = has_privs_of_role(userid, ROLE_PG_READ_ALL_STATS);
+	is_allowed_role = has_cluster_privs_of_role(userid, ROLE_PG_READ_ALL_STATS);
 
 	/* hash table must exist already */
 	if (!pgss || !pgss_hash)
diff --git a/contrib/pgrowlocks/pgrowlocks.c b/contrib/pgrowlocks/pgrowlocks.c
index adbc8279c3f..3c82e634788 100644
--- a/contrib/pgrowlocks/pgrowlocks.c
+++ b/contrib/pgrowlocks/pgrowlocks.c
@@ -104,7 +104,7 @@ pgrowlocks(PG_FUNCTION_ARGS)
 	aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
 								  ACL_SELECT);
 	if (aclresult != ACLCHECK_OK)
-		aclresult = has_privs_of_role(GetUserId(), ROLE_PG_STAT_SCAN_TABLES) ? ACLCHECK_OK : ACLCHECK_NO_PRIV;
+		aclresult = has_privs_of_role(GetUserId(), ROLE_PG_STAT_SCAN_TABLES, MyDatabaseId) ? ACLCHECK_OK : ACLCHECK_NO_PRIV;
 
 	if (aclresult != ACLCHECK_OK)
 		aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 964c819a02d..a40c4f0d5a8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1648,11 +1648,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>
@@ -1739,6 +1738,17 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        to the granted role
       </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 999f657d5c0..b53cae4c14e 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -98,6 +98,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 | INHERIT | SET } { OPTION | TRUE | FALSE } ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
@@ -262,6 +263,27 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    option values.
   </para>
 
+  <para>
+   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>
    Each of the options described below can be set to either
    <literal>TRUE</literal> or <literal>FALSE</literal>. The keyword
@@ -279,7 +301,15 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    do that.  A role is not considered to hold <literal>WITH ADMIN
    OPTION</literal> on itself.  Database superusers can grant or revoke
    membership in any role to anyone. This option defaults to
-   <literal>FALSE</literal>.
+   <literal>FALSE</literal>.  A cluster-wide admin option for the role being granted
+   is required for performing both cluster-wide role membership grants as well as
+   database-specific role membership grants when connected to a database that is not
+   the target of the grant.  A database-specific admin option is only sufficient for
+   performing a role membership grant that is both database-specific and for the
+   currently connected database.  Database-specific admin options can not be
+   used to grant cluster-wide role membership, nor can they be used to grant
+   database-specific role membership for other databases than the currently
+   connected database.
   </para>
 
   <para>
@@ -335,6 +365,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>
 
@@ -456,10 +490,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 8df492281a1..00fce4997b5 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -127,6 +127,7 @@ REVOKE [ GRANT OPTION FOR ]
 
 REVOKE [ { ADMIN | INHERIT | SET } 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 ]
 
@@ -316,6 +317,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 ed18704a9c2..05cadbf2a41 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -391,6 +391,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
@@ -598,6 +603,15 @@ 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>
+
   <warning>
    <para>
     Care should be taken when granting these roles to ensure they are only used
diff --git a/src/backend/backup/basebackup_server.c b/src/backend/backup/basebackup_server.c
index f5c0c61640a..ae0ed71ffca 100644
--- a/src/backend/backup/basebackup_server.c
+++ b/src/backend/backup/basebackup_server.c
@@ -67,7 +67,7 @@ bbsink_server_new(bbsink *next, char *pathname)
 
 	/* Replication permission is not sufficient in this case. */
 	StartTransactionCommand();
-	if (!has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
+	if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to create backup stored on server"),
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 95eb0b12277..9112fbaddbd 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -1142,7 +1142,7 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 
 			iacls.roleid = get_rolespec_oid(rolespec, false);
 
-			if (!has_privs_of_role(GetUserId(), iacls.roleid))
+			if (!has_privs_of_role(GetUserId(), iacls.roleid, MyDatabaseId))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to change default privileges")));
@@ -3430,7 +3430,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 * pg_read_all_data role, which allows read access to all relations.
 	 */
 	if (mask & ACL_SELECT && !(result & ACL_SELECT) &&
-		has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA))
+		has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA, MyDatabaseId))
 		result |= ACL_SELECT;
 
 	/*
@@ -3442,7 +3442,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 */
 	if (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE) &&
 		!(result & (ACL_INSERT | ACL_UPDATE | ACL_DELETE)) &&
-		has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA))
+		has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA, MyDatabaseId))
 		result |= (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE));
 
 	/*
@@ -3453,7 +3453,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
 	 */
 	if (mask & ACL_MAINTAIN &&
 		!(result & ACL_MAINTAIN) &&
-		has_privs_of_role(roleid, ROLE_PG_MAINTAIN))
+		has_privs_of_role(roleid, ROLE_PG_MAINTAIN, MyDatabaseId))
 		result |= ACL_MAINTAIN;
 
 	return result;
@@ -3752,8 +3752,8 @@ pg_namespace_aclmask_ext(Oid nsp_oid, Oid roleid,
 	 * to all schemas.
 	 */
 	if (mask & ACL_USAGE && !(result & ACL_USAGE) &&
-		(has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA) ||
-		 has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA)))
+		(has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA, MyDatabaseId) ||
+		 has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA, MyDatabaseId)))
 		result |= ACL_USAGE;
 	return result;
 }
@@ -4208,7 +4208,7 @@ object_ownercheck(Oid classid, Oid objectid, Oid roleid)
 		table_close(rel, AccessShareLock);
 	}
 
-	return has_privs_of_role(roleid, ownerId);
+	return has_privs_of_role(roleid, ownerId, MyDatabaseId);
 }
 
 /*
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index cfe8c5104b6..fcedbe3a337 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -288,8 +288,8 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
-		relationId == AuthMemMemRoleIndexId ||
-		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemMemRoleDbIndexId ||
+		relationId == AuthMemRoleMemDbIndexId ||
 		relationId == AuthMemOidIndexId ||
 		relationId == AuthMemGrantorIndexId ||
 		relationId == DatabaseNameIndexId ||
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 30807f91904..13525b88c66 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -4773,7 +4773,7 @@ InitializeSearchPath(void)
 									  (Datum) 0);
 
 		/* role membership may affect ACLs */
-		CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
+		CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB,
 									  InvalidationCallback,
 									  (Datum) 0);
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 85a7b7e641a..099d06c832d 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2521,7 +2521,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 							 errmsg("permission denied"),
 							 errdetail("The current user must have the %s attribute.",
 									   "CREATEROLE")));
-				if (!is_admin_of_role(roleid, address.objectId))
+				if (!is_admin_of_role(roleid, address.objectId, InvalidOid))
 					ereport(ERROR,
 							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 							 errmsg("permission denied"),
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb4470..3de2a3be9d2 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -219,7 +219,7 @@ AlterObjectRename_internal(Relation rel, Oid objectId, const char *new_name)
 		Assert(!isnull);
 		ownerId = DatumGetObjectId(datum);
 
-		if (!has_privs_of_role(GetUserId(), DatumGetObjectId(ownerId)))
+		if (!has_privs_of_role(GetUserId(), DatumGetObjectId(ownerId), MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_object_type(classId, objectId),
 						   old_name);
 
@@ -739,7 +739,7 @@ AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid)
 		Assert(!isnull);
 		ownerId = DatumGetObjectId(owner);
 
-		if (!has_privs_of_role(GetUserId(), ownerId))
+		if (!has_privs_of_role(GetUserId(), ownerId, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_object_type(classId, objid),
 						   NameStr(*(DatumGetName(name))));
 
@@ -961,7 +961,7 @@ AlterObjectOwner_internal(Oid classId, Oid objectId, Oid new_ownerId)
 		if (!superuser())
 		{
 			/* must be owner */
-			if (!has_privs_of_role(GetUserId(), old_ownerId))
+			if (!has_privs_of_role(GetUserId(), old_ownerId, MyDatabaseId))
 			{
 				char	   *objname;
 				char		namebuf[NAMEDATALEN];
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 03eb7a4ebac..5ae69286482 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -78,7 +78,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		if (stmt->is_program)
 		{
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_EXECUTE_SERVER_PROGRAM))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY to or from an external program"),
@@ -89,7 +89,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		}
 		else
 		{
-			if (is_from && !has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+			if (is_from && !has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY from a file"),
@@ -98,7 +98,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 						 errhint("Anyone can COPY to stdout or from stdin. "
 								 "psql's \\copy command also works for anyone.")));
 
-			if (!is_from && !has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
+			if (!is_from && !has_cluster_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied to COPY to a file"),
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index aa91a396967..73e88cf2b20 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -48,6 +48,7 @@
 #include "commands/defrem.h"
 #include "commands/seclabel.h"
 #include "commands/tablespace.h"
+#include "commands/user.h"
 #include "common/file_perm.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
@@ -1774,6 +1775,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/indexcmds.c b/src/backend/commands/indexcmds.c
index e33ad815295..da5e698dd68 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -3120,7 +3120,7 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
 		objectOid = get_namespace_oid(objectName, false);
 
 		if (!object_ownercheck(NamespaceRelationId, objectOid, GetUserId()) &&
-			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
+			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SCHEMA,
 						   objectName);
 	}
@@ -3133,7 +3133,7 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("can only reindex the currently open database")));
 		if (!object_ownercheck(DatabaseRelationId, objectOid, GetUserId()) &&
-			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
+			!has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN, MyDatabaseId))
 			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
 						   get_database_name(objectOid));
 	}
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 02ccc636b80..670590470e2 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -611,7 +611,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 	 * attempts to access arbitrary network destinations, so require the user
 	 * to have been specifically authorized to create subscriptions.
 	 */
-	if (!has_privs_of_role(owner, ROLE_PG_CREATE_SUBSCRIPTION))
+	if (!has_privs_of_role(owner, ROLE_PG_CREATE_SUBSCRIPTION, MyDatabaseId))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to create subscription"),
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index e7ade898a47..f56cc3eabaa 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -92,15 +92,15 @@ check_password_hook_type check_password_hook = NULL;
 
 static void AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
-						Oid grantorId, GrantRoleOptions *popt);
+						Oid grantorId, GrantRoleOptions *popt, Oid db_id);
 static void DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 						List *memberSpecs, List *memberIds,
 						Oid grantorId, GrantRoleOptions *popt,
-						DropBehavior behavior);
+						DropBehavior behavior, Oid db_id);
 static void check_role_membership_authorization(Oid currentUserId, Oid roleid,
-												bool is_grant);
+												bool is_grant, Oid db_id);
 static Oid	check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
-							   bool is_grant);
+							   bool is_grant, Oid db_id);
 static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
 static bool plan_single_revoke(CatCList *memlist,
 							   RevokeRoleGrantAction *actions,
@@ -517,11 +517,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 			char	   *oldrolename = NameStr(oldroleform->rolname);
 
 			/* can only add this role to roles for which you have rights */
-			check_role_membership_authorization(currentUserId, oldroleid, true);
+			check_role_membership_authorization(currentUserId, oldroleid,
+												true, InvalidOid);
 			AddRoleMems(currentUserId, oldrolename, oldroleid,
 						thisrole_list,
 						thisrole_oidlist,
-						InvalidOid, &popt);
+						InvalidOid, &popt, InvalidOid);
 
 			ReleaseSysCache(oldroletup);
 		}
@@ -557,7 +558,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 		AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
 					memberSpecs, memberIds,
-					BOOTSTRAP_SUPERUSERID, &poptself);
+					BOOTSTRAP_SUPERUSERID, &poptself, InvalidOid);
 
 		/*
 		 * We must make the implicit grant visible to the code below, else the
@@ -577,7 +578,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		if (createrole_self_grant_enabled)
 			AddRoleMems(currentUserId, stmt->role, roleid,
 						memberSpecs, memberIds,
-						currentUserId, &createrole_self_grant_options);
+						currentUserId, &createrole_self_grant_options, InvalidOid);
 	}
 
 	/*
@@ -589,12 +590,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	AddRoleMems(currentUserId, stmt->role, roleid,
 				rolemembers, roleSpecsToIds(rolemembers),
-				InvalidOid, &popt);
+				InvalidOid, &popt, InvalidOid);
 	popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
 	popt.admin = true;
 	AddRoleMems(currentUserId, stmt->role, roleid,
 				adminmembers, roleSpecsToIds(adminmembers),
-				InvalidOid, &popt);
+				InvalidOid, &popt, InvalidOid);
 
 	/* Post creation hook for new role */
 	InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -771,7 +772,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	 * and also ADMIN OPTION on the role.
 	 */
 	if (!have_createrole_privilege() ||
-		!is_admin_of_role(GetUserId(), roleid))
+		!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 	{
 		/* things an unprivileged user certainly can't do */
 		if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
@@ -818,7 +819,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 
 	/* To add members to a role, you need ADMIN OPTION. */
-	if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+	if (drolemembers && !is_admin_of_role(currentUserId, roleid, InvalidOid))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to alter role"),
@@ -977,11 +978,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		if (stmt->action == +1) /* add members to role */
 			AddRoleMems(currentUserId, rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						InvalidOid, &popt);
+						InvalidOid, &popt, InvalidOid);
 		else if (stmt->action == -1)	/* drop members from role */
 			DelRoleMems(currentUserId, rolename, roleid,
 						rolemembers, roleSpecsToIds(rolemembers),
-						InvalidOid, &popt, DROP_RESTRICT);
+						InvalidOid, &popt, DROP_RESTRICT, InvalidOid);
 	}
 
 	/*
@@ -1036,7 +1037,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 		else
 		{
 			if ((!have_createrole_privilege() ||
-				 !is_admin_of_role(GetUserId(), roleid))
+				 !is_admin_of_role(GetUserId(), roleid, InvalidOid))
 				&& roleid != GetUserId())
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1171,7 +1172,7 @@ DropRole(DropRoleStmt *stmt)
 					 errmsg("permission denied to drop role"),
 					 errdetail("Only roles with the %s attribute may drop roles with the %s attribute.",
 							   "SUPERUSER", "SUPERUSER")));
-		if (!is_admin_of_role(GetUserId(), roleid))
+		if (!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to drop role"),
@@ -1212,7 +1213,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)))
@@ -1232,7 +1233,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)))
@@ -1425,7 +1426,7 @@ RenameRole(const char *oldname, const char *newname)
 	else
 	{
 		if (!have_createrole_privilege() ||
-			!is_admin_of_role(GetUserId(), roleid))
+			!is_admin_of_role(GetUserId(), roleid, InvalidOid))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to rename role"),
@@ -1485,6 +1486,7 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 	ListCell   *item;
 	GrantRoleOptions popt;
 	Oid			currentUserId = GetUserId();
+	Oid			dbid;
 
 	/* Parse options list. */
 	InitGrantRoleOptions(&popt);
@@ -1533,6 +1535,14 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 
 	grantee_ids = roleSpecsToIds(stmt->grantee_roles);
 
+	/* Lookup OID of database, if specified. */
+	if (stmt->database == NULL)
+		dbid = InvalidOid;
+	else if (strcmp(stmt->database, "") == 0)
+		dbid = MyDatabaseId;
+	else
+		dbid = get_database_oid(stmt->database, false);
+
 	/* AccessShareLock is enough since we aren't modifying pg_authid */
 	pg_authid_rel = table_open(AuthIdRelationId, AccessShareLock);
 
@@ -1557,15 +1567,15 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
 
 		roleid = get_role_oid(rolename, false);
 		check_role_membership_authorization(currentUserId,
-											roleid, stmt->is_grant);
+											roleid, stmt->is_grant, dbid);
 		if (stmt->is_grant)
 			AddRoleMems(currentUserId, rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						grantor, &popt);
+						grantor, &popt, dbid);
 		else
 			DelRoleMems(currentUserId, rolename, roleid,
 						stmt->grantee_roles, grantee_ids,
-						grantor, &popt, stmt->behavior);
+						grantor, &popt, stmt->behavior, dbid);
 	}
 
 	/*
@@ -1590,7 +1600,7 @@ DropOwnedObjects(DropOwnedStmt *stmt)
 	{
 		Oid			roleid = lfirst_oid(cell);
 
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_privs_of_role(GetUserId(), roleid, MyDatabaseId))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to drop objects"),
@@ -1619,7 +1629,7 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
 	{
 		Oid			roleid = lfirst_oid(cell);
 
-		if (!has_privs_of_role(GetUserId(), roleid))
+		if (!has_privs_of_role(GetUserId(), roleid, MyDatabaseId))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to reassign objects"),
@@ -1630,7 +1640,7 @@ ReassignOwnedObjects(ReassignOwnedStmt *stmt)
 	/* Must have privileges on the receiving side too */
 	newrole = get_rolespec_oid(stmt->newrole, false);
 
-	if (!has_privs_of_role(GetUserId(), newrole))
+	if (!has_privs_of_role(GetUserId(), newrole, MyDatabaseId))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to reassign objects"),
@@ -1676,11 +1686,12 @@ roleSpecsToIds(List *memberNames)
  * grantorId: OID that should be recorded as having granted the membership
  * (InvalidOid if not set explicitly)
  * popt: information about grant options
+ * db_id: OID of the database in which membership should be granted
  */
 static void
 AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			Oid grantorId, GrantRoleOptions *popt)
+			Oid grantorId, GrantRoleOptions *popt, Oid db_id)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1690,7 +1701,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	Assert(list_length(memberSpecs) == list_length(memberIds));
 
 	/* Validate grantor (and resolve implicit grantor if not specified). */
-	grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
+	grantorId = check_role_grantor(currentUserId, roleid, grantorId, true, db_id);
 
 	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
 	pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -1769,7 +1780,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 		int			i;
 
 		/* Get the list of members for this role. */
-		memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+		memlist = SearchSysCacheList1(AUTHMEMROLEMEMDB,
 									  ObjectIdGetDatum(roleid));
 
 		/*
@@ -1834,12 +1845,15 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			ObjectIdGetDatum(memberid);
 		new_record[Anum_pg_auth_members_grantor - 1] =
 			ObjectIdGetDatum(grantorId);
+		new_record[Anum_pg_auth_members_dbid - 1] =
+			ObjectIdGetDatum(db_id);
 
 		/* Find any existing tuple */
-		authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
+		authmem_tuple = SearchSysCache4(AUTHMEMROLEMEMDB,
 										ObjectIdGetDatum(roleid),
 										ObjectIdGetDatum(memberid),
-										ObjectIdGetDatum(grantorId));
+										ObjectIdGetDatum(grantorId),
+										ObjectIdGetDatum(db_id));
 
 		/*
 		 * If we found a tuple, update it with new option values, unless there
@@ -1886,10 +1900,16 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 
 			if (!at_least_one_change)
 			{
-				ereport(NOTICE,
-						(errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
-								get_rolespec_name(memberRole), rolename,
-								GetUserNameFromId(grantorId, false))));
+				if (OidIsValid(db_id))
+					ereport(NOTICE,
+							(errmsg("role \"%s\" has already been granted membership in role \"%s\" in database \"%s\" by role \"%s\"",
+									get_rolespec_name(memberRole), rolename, get_database_name(db_id),
+									GetUserNameFromId(grantorId, false))));
+				else
+					ereport(NOTICE,
+							(errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+									get_rolespec_name(memberRole), rolename,
+									GetUserNameFromId(grantorId, false))));
 				ReleaseSysCache(authmem_tuple);
 				continue;
 			}
@@ -1974,11 +1994,12 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
  * grantorId: who is revoking the membership
  * popt: information about grant options
  * behavior: RESTRICT or CASCADE behavior for recursive removal
+ * db_id: OID of the database in which membership should be removed
  */
 static void
 DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 			List *memberSpecs, List *memberIds,
-			Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
+			Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior, Oid db_id)
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
@@ -1991,7 +2012,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	Assert(list_length(memberSpecs) == list_length(memberIds));
 
 	/* Validate grantor (and resolve implicit grantor if not specified). */
-	grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+	grantorId = check_role_grantor(currentUserId, roleid, grantorId, false, db_id);
 
 	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
 	pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -2004,7 +2025,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
 	LockSharedObject(AuthIdRelationId, roleid, 0,
 					 ShareUpdateExclusiveLock);
 
-	memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+	memlist = SearchSysCacheList1(AUTHMEMROLEMEMDB, ObjectIdGetDatum(roleid));
 	actions = initialize_revoke_actions(memlist);
 
 	/*
@@ -2109,7 +2130,7 @@ DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
  */
 static void
 check_role_membership_authorization(Oid currentUserId, Oid roleid,
-									bool is_grant)
+									bool is_grant, Oid dbid)
 {
 	/*
 	 * The charter of pg_database_owner is to have exactly one, implicit,
@@ -2152,7 +2173,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
 		/*
 		 * Otherwise, must have admin option on the role to be changed.
 		 */
-		if (!is_admin_of_role(currentUserId, roleid))
+		if (!is_admin_of_role(currentUserId, roleid, dbid))
 		{
 			if (is_grant)
 				ereport(ERROR,
@@ -2202,7 +2223,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
  * the operation.
  */
 static Oid
-check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant, Oid db_id)
 {
 	/* If the grantor ID was not specified, pick one to use. */
 	if (!OidIsValid(grantorId))
@@ -2226,7 +2247,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 		 * established that the current user has permission to perform the
 		 * operation.)
 		 */
-		grantorId = select_best_admin(currentUserId, roleid);
+		grantorId = select_best_admin(currentUserId, roleid, db_id);
 		if (!OidIsValid(grantorId))
 			elog(ERROR, "no possible grantors");
 		return grantorId;
@@ -2243,7 +2264,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 	 */
 	if (is_grant)
 	{
-		if (!has_privs_of_role(currentUserId, grantorId))
+		if (!has_privs_of_role(currentUserId, grantorId, db_id))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to grant privileges as role \"%s\"",
@@ -2252,7 +2273,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 							   GetUserNameFromId(grantorId, false))));
 
 		if (grantorId != BOOTSTRAP_SUPERUSERID &&
-			select_best_admin(grantorId, roleid) != grantorId)
+			select_best_admin(grantorId, roleid, db_id) != grantorId)
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to grant privileges as role \"%s\"",
@@ -2262,7 +2283,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
 	}
 	else
 	{
-		if (!has_privs_of_role(currentUserId, grantorId))
+		if (!has_privs_of_role(currentUserId, grantorId, db_id))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to revoke privileges granted by role \"%s\"",
@@ -2579,3 +2600,42 @@ assign_createrole_self_grant(const char *newval, void *extra)
 	createrole_self_grant_options.set =
 		(options & GRANT_ROLE_SPECIFIED_SET) != 0;
 }
+
+/*
+ * DropDatabaseSpecificRoles
+ *
+ * Delete pg_auth_members entries corresponding to a database that's being
+ * dropped.
+ */
+void
+DropDatabaseSpecificRoles(Oid databaseId)
+{
+	Relation	pg_authmem_rel;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+	HeapTuple	tup;
+
+	pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_members_dbid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(databaseId));
+
+	sscan = systable_beginscan(pg_authmem_rel, AuthMemMemRoleDbIndexId,
+							   true, NULL, 1, &scankey);
+
+	while (HeapTupleIsValid(tup = systable_getnext(sscan)))
+	{
+		Form_pg_auth_members authmem_form;
+
+		authmem_form = (Form_pg_auth_members) GETSTRUCT(tup);
+		deleteSharedDependencyRecordsFor(AuthMemRelationId,
+										 authmem_form->oid, 0);
+		CatalogTupleDelete(pg_authmem_rel, &tup->t_self);
+	}
+
+	systable_endscan(sscan);
+
+	table_close(pg_authmem_rel, RowExclusiveLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7b..8e0ad919510 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -367,7 +367,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>		parameter_name
 %type <list>	OptSchemaEltList parameter_name_list
@@ -7930,6 +7930,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; }
@@ -7942,32 +7947,34 @@ opt_grant_grant_option:
  *****************************************************************************/
 
 GrantRoleStmt:
-			GRANT privilege_list TO role_list opt_granted_by
+			GRANT privilege_list TO role_list opt_grant_in_database opt_granted_by
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 
 					n->is_grant = true;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
+					n->database = $5;
 					n->opt = NIL;
-					n->grantor = $5;
+					n->grantor = $6;
 					$$ = (Node *) n;
 				}
-		  | GRANT privilege_list TO role_list WITH grant_role_opt_list opt_granted_by
+		  | GRANT privilege_list TO role_list opt_grant_in_database WITH grant_role_opt_list opt_granted_by
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 
 					n->is_grant = true;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->opt = $6;
-					n->grantor = $7;
+					n->database = $5;
+					n->opt = $7;
+					n->grantor = $8;
 					$$ = (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);
 
@@ -7975,11 +7982,12 @@ RevokeRoleStmt:
 					n->opt = NIL;
 					n->granted_roles = $2;
 					n->grantee_roles = $4;
-					n->grantor = $5;
-					n->behavior = $6;
+					n->database = $5;
+					n->grantor = $6;
+					n->behavior = $7;
 					$$ = (Node *) n;
 				}
-			| REVOKE ColId OPTION FOR privilege_list FROM role_list opt_granted_by opt_drop_behavior
+			| REVOKE ColId OPTION FOR privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior
 				{
 					GrantRoleStmt *n = makeNode(GrantRoleStmt);
 					DefElem *opt;
@@ -7990,8 +7998,9 @@ RevokeRoleStmt:
 					n->opt = list_make1(opt);
 					n->granted_roles = $5;
 					n->grantee_roles = $7;
-					n->grantor = $8;
-					n->behavior = $9;
+					n->database = $8;
+					n->grantor = $9;
+					n->behavior = $10;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index a27aee63def..4f3afe069ca 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -1465,7 +1465,7 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS)
 	/* Fetch values */
 	values[0] = Int32GetDatum(pid);
 
-	if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		/*
 		 * Only superusers and roles with privileges of pg_read_all_stats can
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 866b69ec855..4f6327c7518 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -3933,7 +3933,7 @@ pg_stat_get_wal_senders(PG_FUNCTION_ARGS)
 
 		values[0] = Int32GetDatum(pid);
 
-		if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 		{
 			/*
 			 * Only superusers and roles with privileges of pg_read_all_stats
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index 59fd305dd7b..84cace07f94 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -924,7 +924,7 @@ check_role_for_policy(ArrayType *policy_roles, Oid user_id)
 
 	for (i = 0; i < ARR_DIMS(policy_roles)[0]; i++)
 	{
-		if (has_privs_of_role(user_id, roles[i]))
+		if (has_privs_of_role(user_id, roles[i], MyDatabaseId))
 			return true;
 	}
 
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 36610a1c7e7..77558d4c88e 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -3899,8 +3899,8 @@ TerminateOtherDBBackends(Oid databaseId)
 							 errdetail("Only roles with the %s attribute may terminate processes of roles with the %s attribute.",
 									   "SUPERUSER", "SUPERUSER")));
 
-				if (!has_privs_of_role(GetUserId(), proc->roleId) &&
-					!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
+				if (!has_privs_of_role(GetUserId(), proc->roleId, databaseId) &&
+					!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND, databaseId))
 					ereport(ERROR,
 							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 							 errmsg("permission denied to terminate process"),
diff --git a/src/backend/storage/ipc/signalfuncs.c b/src/backend/storage/ipc/signalfuncs.c
index aa729a36e39..50e6ed3a500 100644
--- a/src/backend/storage/ipc/signalfuncs.c
+++ b/src/backend/storage/ipc/signalfuncs.c
@@ -92,14 +92,14 @@ pg_signal_backend(int pid, int sig)
 
 		if (procStatus && procStatus->st_backendType == B_AUTOVAC_WORKER)
 		{
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_AUTOVACUUM_WORKER))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_AUTOVACUUM_WORKER))
 				return SIGNAL_BACKEND_NOAUTOVAC;
 		}
 		else if (!superuser())
 			return SIGNAL_BACKEND_NOSUPERUSER;
 	}
-	else if (!has_privs_of_role(GetUserId(), proc->roleId) &&
-			 !has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
+	else if (!has_cluster_privs_of_role(GetUserId(), proc->roleId) &&
+			 !has_cluster_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
 		return SIGNAL_BACKEND_NOPERMISSION;
 
 	/*
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index b2ea8125c92..900ccc292d1 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -940,7 +940,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_CheckPointStmt:
-			if (!has_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT))
+			if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				/* translator: %s is name of a SQL command, eg CHECKPOINT */
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 2a716cc6b7f..d2f186da249 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -1395,7 +1395,7 @@ aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 
 	/* Owner always implicitly has all grant options */
 	if ((mask & ACLITEM_ALL_GOPTION_BITS) &&
-		has_privs_of_role(roleid, ownerId))
+		has_privs_of_role(roleid, ownerId, MyDatabaseId))
 	{
 		result = mask & ACLITEM_ALL_GOPTION_BITS;
 		if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
@@ -1438,7 +1438,7 @@ aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 			continue;			/* already checked it */
 
 		if ((aidata->ai_privs & remaining) &&
-			has_privs_of_role(roleid, aidata->ai_grantee))
+			has_privs_of_role(roleid, aidata->ai_grantee, MyDatabaseId))
 		{
 			result |= aidata->ai_privs & mask;
 			if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0))
@@ -4995,7 +4995,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 {
 	if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
 	{
-		if (is_admin_of_role(roleid, role_oid))
+		if (is_admin_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_CREATE)
@@ -5005,7 +5005,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
 	}
 	if (mode & ACL_USAGE)
 	{
-		if (has_privs_of_role(roleid, role_oid))
+		if (has_privs_of_role(roleid, role_oid, MyDatabaseId))
 			return ACLCHECK_OK;
 	}
 	if (mode & ACL_SET)
@@ -5034,7 +5034,7 @@ initialize_acl(void)
 		 * of pg_auth_members (for roles_is_member_of()) pg_database (for
 		 * roles_is_member_of())
 		 */
-		CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
+		CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB,
 									  RoleMembershipCacheCallback,
 									  (Datum) 0);
 		CacheRegisterSyscacheCallback(AUTHOID,
@@ -5114,7 +5114,62 @@ roles_list_append(List *roles_list, bloom_filter **bf, Oid role)
 }
 
 /*
- * Get a list of roles that the specified roleid is a member of
+ * Append role memberships to the list of roles
+ */
+static void
+append_role_memberships(List *roles_list, enum RoleRecurseType type,
+						Oid admin_of, Oid *admin_role, Oid memberid,
+						Oid databaseId, bloom_filter *bf)
+{
+	CatCList   *memlist;
+	int			i;
+	Oid			targetDatabaseId;
+
+	if (!OidIsValid(databaseId))
+		targetDatabaseId = InvalidOid;
+	else
+		targetDatabaseId = MyDatabaseId;
+
+	memlist = SearchSysCacheList2(AUTHMEMMEMDBROLE,
+								  ObjectIdGetDatum(memberid),
+								  ObjectIdGetDatum(targetDatabaseId));
+	for (i = 0; i < memlist->n_members; i++)
+	{
+		HeapTuple	tup = &memlist->members[i]->tuple;
+		Form_pg_auth_members form = (Form_pg_auth_members) GETSTRUCT(tup);
+		Oid			otherid = form->roleid;
+		Oid			db_id = form->dbid;
+
+		/*
+		 * While otherid==InvalidOid shouldn't appear in the catalog, the
+		 * OidIsValid() avoids crashing if that arises.
+		 */
+		if (otherid == admin_of && form->admin_option &&
+			db_id == databaseId &&
+			OidIsValid(admin_of) && !OidIsValid(*admin_role))
+			*admin_role = memberid;
+
+		/* If we're supposed to ignore non-heritable grants, do so. */
+		if (type == ROLERECURSE_PRIVS && db_id == databaseId && !form->inherit_option)
+			continue;
+
+		/* If we're supposed to ignore non-SET grants, do so. */
+		if (type == ROLERECURSE_SETROLE && db_id == databaseId && !form->set_option)
+			continue;
+
+		/*
+		 * 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 = roles_list_append(roles_list, &bf, otherid);
+	}
+	ReleaseSysCacheList(memlist);
+}
+
+/*
+ * Get a list of roles that the specified roleid is a member of, possibly
+ * scoped to specified database with databaseId.
  *
  * Type ROLERECURSE_MEMBERS recurses through all grants; ROLERECURSE_PRIVS
  * recurses only through inheritable grants; and ROLERECURSE_SETROLE recurses
@@ -5134,7 +5189,7 @@ roles_list_append(List *roles_list, bloom_filter **bf, Oid role)
  */
 static List *
 roles_is_member_of(Oid roleid, enum RoleRecurseType type,
-				   Oid admin_of, Oid *admin_role)
+				   Oid admin_of, Oid *admin_role, Oid databaseId)
 {
 	Oid			dba;
 	List	   *roles_list;
@@ -5157,7 +5212,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 	 * ROLE_PG_READ_ALL_SETTINGS for a physical walsender SHOW command.  In
 	 * that case, no role gets pg_database_owner.
 	 */
-	if (!OidIsValid(MyDatabaseId))
+	if (!OidIsValid(MyDatabaseId))	/* XXX or databaseId? */
 		dba = InvalidOid;
 	else
 	{
@@ -5186,42 +5241,18 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 	foreach(l, roles_list)
 	{
 		Oid			memberid = lfirst_oid(l);
-		CatCList   *memlist;
-		int			i;
 
 		/* 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;
-			Form_pg_auth_members form = (Form_pg_auth_members) GETSTRUCT(tup);
-			Oid			otherid = form->roleid;
-
-			/*
-			 * While otherid==InvalidOid shouldn't appear in the catalog, the
-			 * OidIsValid() avoids crashing if that arises.
-			 */
-			if (otherid == admin_of && form->admin_option &&
-				OidIsValid(admin_of) && !OidIsValid(*admin_role))
-				*admin_role = memberid;
-
-			/* If we're supposed to ignore non-heritable grants, do so. */
-			if (type == ROLERECURSE_PRIVS && !form->inherit_option)
-				continue;
+		append_role_memberships(roles_list, type, admin_of, admin_role,
+								memberid, InvalidOid, bf);
 
-			/* If we're supposed to ignore non-SET grants, do so. */
-			if (type == ROLERECURSE_SETROLE && !form->set_option)
-				continue;
-
-			/*
-			 * 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 = roles_list_append(roles_list, &bf, otherid);
-		}
-		ReleaseSysCacheList(memlist);
+		/*
+		 * Find roles that memberid is directly a member of in the current
+		 * database
+		 */
+		if (OidIsValid(databaseId))
+			append_role_memberships(roles_list, type, admin_of, admin_role,
+									memberid, databaseId, bf);
 
 		/* implement pg_database_owner implicit membership */
 		if (memberid == dba && OidIsValid(dba))
@@ -5257,7 +5288,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 
 
 /*
- * Does member have the privileges of role (directly or indirectly)?
+ * Does member have the privileges of role (directly or indirectly) in
+ * specified database or cluster-wise?
  *
  * This is defined not to recurse through grants that are not inherited,
  * and only inherited grants confer the associated privileges automatically.
@@ -5265,7 +5297,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
  * See also member_can_set_role, below.
  */
 bool
-has_privs_of_role(Oid member, Oid role)
+has_privs_of_role(Oid member, Oid role, Oid database)
 {
 	/* Fast path for simple case */
 	if (member == role)
@@ -5280,10 +5312,21 @@ 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, database),
 						   role);
 }
 
+
+/*
+ * Does member have the privileges of role *cluster-wide*?
+ *
+ */
+bool
+has_cluster_privs_of_role(Oid member, Oid role)
+{
+	return has_privs_of_role(member, role, InvalidOid);
+}
+
 /*
  * Can member use SET ROLE to this role?
  *
@@ -5314,7 +5357,7 @@ member_can_set_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_SETROLE,
-											  InvalidOid, NULL),
+											  InvalidOid, NULL, MyDatabaseId),
 						   role);
 }
 
@@ -5360,7 +5403,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);
 }
 
@@ -5384,7 +5427,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);
 }
 
@@ -5395,7 +5438,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)
 {
 	Oid			admin_role;
 
@@ -5406,7 +5449,7 @@ is_admin_of_role(Oid member, Oid role)
 	if (member == role)
 		return false;
 
-	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+	(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role, databaseId);
 	return OidIsValid(admin_role);
 }
 
@@ -5420,7 +5463,7 @@ is_admin_of_role(Oid member, Oid role)
  * over more indirect inheritance.
  */
 Oid
-select_best_admin(Oid member, Oid role)
+select_best_admin(Oid member, Oid role, Oid databaseId)
 {
 	Oid			admin_role;
 
@@ -5428,7 +5471,7 @@ select_best_admin(Oid member, Oid role)
 	if (member == role)
 		return InvalidOid;
 
-	(void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+	(void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role, databaseId);
 	return admin_role;
 }
 
@@ -5504,7 +5547,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/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index e63e99c1416..2f31ecb0c4e 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -130,7 +130,7 @@ calculate_database_size(Oid dbOid)
 	 */
 	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(), ACL_CONNECT);
 	if (aclresult != ACLCHECK_OK &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		aclcheck_error(aclresult, OBJECT_DATABASE,
 					   get_database_name(dbOid));
@@ -214,7 +214,7 @@ calculate_tablespace_size(Oid tblspcOid)
 	 * it is default for current database.
 	 */
 	if (tblspcOid != MyDatabaseTableSpace &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 	{
 		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
 		if (aclresult != ACLCHECK_OK)
diff --git a/src/backend/utils/adt/genfile.c b/src/backend/utils/adt/genfile.c
index 24b95c32b78..5f2a4b5997f 100644
--- a/src/backend/utils/adt/genfile.c
+++ b/src/backend/utils/adt/genfile.c
@@ -63,7 +63,7 @@ convert_and_check_filename(text *arg)
 	 * access any files on the server as the PG user, so no need to do any
 	 * further checks here.
 	 */
-	if (has_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+	if (has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
 		return filename;
 
 	/*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f7b50e0b5af..23a3e4ea9e0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -34,7 +34,7 @@
 
 #define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))
 
-#define HAS_PGSTAT_PERMISSIONS(role)	 (has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) || has_privs_of_role(GetUserId(), role))
+#define HAS_PGSTAT_PERMISSIONS(role)	 (has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) || has_cluster_privs_of_role(GetUserId(), role))
 
 #define PG_STAT_GET_RELENTRY_INT64(stat)						\
 Datum															\
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 10276aa1db1..e8b601301c9 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1223,7 +1223,7 @@ IndexScanOK(CatCache *cache)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHMEMMEMROLE:
+		case AUTHMEMMEMDBROLE:
 		case DATABASEOID:
 
 			/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c326f687eb4..5a9223c4539 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4211,7 +4211,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/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d0..85f5dec665e 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -922,7 +922,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 					 errmsg("remaining connection slots are reserved for roles with the %s attribute",
 							"SUPERUSER")));
 
-		if (!has_privs_of_role(GetUserId(), ROLE_PG_USE_RESERVED_CONNECTIONS))
+		if (!has_cluster_privs_of_role(GetUserId(), ROLE_PG_USE_RESERVED_CONNECTIONS))
 			ereport(FATAL,
 					(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
 					 errmsg("remaining connection slots are reserved for roles with privileges of the \"%s\" role",
diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c
index 9c9edd3d2f5..2a845564f1f 100644
--- a/src/backend/utils/misc/guc_funcs.c
+++ b/src/backend/utils/misc/guc_funcs.c
@@ -581,7 +581,7 @@ bool
 ConfigOptionIsVisible(struct config_generic *conf)
 {
 	if ((conf->flags & GUC_SUPERUSER_ONLY) &&
-		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
+		!has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
 		return false;
 	else
 		return true;
@@ -784,7 +784,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values)
 	 * insufficiently-privileged users.
 	 */
 	if (conf->source == PGC_S_FILE &&
-		has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
+		has_cluster_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS))
 	{
 		values[14] = conf->sourcefile;
 		snprintf(buffer, sizeof(buffer), "%d", conf->sourceline);
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb2956..67ebbf24f0d 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -58,7 +58,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 dumpRoleGUCPrivs(PGconn *conn);
 static void dropTablespaces(PGconn *conn);
 static void dumpTablespaces(PGconn *conn);
@@ -594,7 +594,7 @@ main(int argc, char *argv[])
 			dumpRoles(conn);
 
 			/* Dump role memberships */
-			dumpRoleMembership(conn);
+			dumpRoleMembership(conn, "0");
 
 			/* Dump role GUC privileges */
 			if (server_version >= 150000 && !skip_acls)
@@ -955,7 +955,7 @@ dumpRoles(PGconn *conn)
  * no membership yet.
  */
 static void
-dumpRoleMembership(PGconn *conn)
+dumpRoleMembership(PGconn *conn, const char *databaseId)
 {
 	PQExpBuffer buf = createPQExpBuffer();
 	PQExpBuffer optbuf = createPQExpBuffer();
@@ -995,7 +995,8 @@ dumpRoleMembership(PGconn *conn)
 					  "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,4", role_catalog, role_catalog, role_catalog);
+					  " AND a.dbid = %s "
+					  "ORDER BY 1,2,4", role_catalog, role_catalog, role_catalog, databaseId);
 	res = executeQuery(conn, buf->data);
 	i_inherit_option = PQfnumber(res, "inherit_option");
 	i_set_option = PQfnumber(res, "set_option");
@@ -1110,6 +1111,8 @@ dumpRoleMembership(PGconn *conn)
 				resetPQExpBuffer(optbuf);
 				fprintf(OPF, "GRANT %s", fmtId(role));
 				fprintf(OPF, " TO %s", fmtId(member));
+				if (strcmp(databaseId, "0") != 0)
+					fprintf(OPF, " IN CURRENT DATABASE");
 				if (*admin_option == 't')
 					appendPQExpBufferStr(optbuf, "ADMIN OPTION");
 				if (dump_grant_options)
@@ -1504,7 +1507,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 AND datconnlimit != -2 "
 					   "ORDER BY (datname <> 'template1'), datname");
@@ -1515,6 +1518,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;
 
@@ -1555,6 +1559,10 @@ dumpDatabases(PGconn *conn)
 		else
 			create_opts = "--create";
 
+		/* Dump database-specific roles if server is running 16.0 or later */
+		if (server_version >= 160000)
+			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 5f4b30756ca..aa1b80bacad 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -36,6 +36,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
 	bool		admin_option;	/* granted with admin option? */
 	bool		inherit_option; /* exercise privileges without SET ROLE? */
 	bool		set_option;		/* use SET ROLE to the target role? */
+	Oid			dbid BKI_LOOKUP_OPT(pg_database);	/* ID of a database this
+													 * mapping is effective in */
 } FormData_pg_auth_members;
 
 /* ----------------
@@ -46,11 +48,11 @@ 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_oid_index, 6303, AuthMemOidIndexId, pg_auth_members, btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, pg_auth_members, btree(roleid oid_ops, member oid_ops, grantor oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, pg_auth_members, btree(member oid_ops, roleid oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_db_index, 2694, AuthMemRoleMemDbIndexId, pg_auth_members, btree(roleid oid_ops, member oid_ops, grantor oid_ops, dbid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_db_role_index, 2695, AuthMemMemRoleDbIndexId, pg_auth_members, btree(member oid_ops, dbid oid_ops, roleid oid_ops, grantor oid_ops));
 DECLARE_INDEX(pg_auth_members_grantor_index, 6302, AuthMemGrantorIndexId, pg_auth_members, btree(grantor oid_ops));
 
-MAKE_SYSCACHE(AUTHMEMROLEMEM, pg_auth_members_role_member_index, 8);
-MAKE_SYSCACHE(AUTHMEMMEMROLE, pg_auth_members_member_role_index, 8);
+MAKE_SYSCACHE(AUTHMEMROLEMEMDB, pg_auth_members_role_member_db_index, 8);
+MAKE_SYSCACHE(AUTHMEMMEMDBROLE, pg_auth_members_member_db_role_index, 8);
 
 #endif							/* PG_AUTH_MEMBERS_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 97dcb93791b..5444b740008 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -35,6 +35,7 @@ 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);
 
 extern bool check_createrole_self_grant(char **newval, void **extra,
 										GucSource source);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1c314cd9074..093dbb4766e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2557,6 +2557,9 @@ 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 means
+								 * global, "" for the current database,
+								 * otherwise a named database */
 	bool		is_grant;		/* true = GRANT, false = REVOKE */
 	List	   *opt;			/* options e.g. WITH GRANT 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 731d84b2a93..d6e7259e8a2 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,13 +209,14 @@ extern AclMode aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 					   AclMode mask, AclMaskHow how);
 extern int	aclmembers(const Acl *acl, Oid **roleids);
 
-extern bool has_privs_of_role(Oid member, Oid role);
+extern bool has_privs_of_role(Oid member, Oid role, Oid database);
+extern bool has_cluster_privs_of_role(Oid member, Oid role);
 extern bool member_can_set_role(Oid member, Oid role);
 extern void check_can_set_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 Oid	select_best_admin(Oid member, Oid role);
+extern bool is_admin_of_role(Oid member, Oid role, Oid databaseId);
+extern Oid	select_best_admin(Oid member, Oid role, Oid databaseId);
 extern Oid	get_role_oid(const char *rolname, bool missing_ok);
 extern Oid	get_role_oid_or_public(const char *rolname);
 extern Oid	get_rolespec_oid(const RoleSpec *role, bool missing_ok);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..79fb69059b6 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}
diff --git a/src/test/regress/expected/privs_in_db.out b/src/test/regress/expected/privs_in_db.out
new file mode 100644
index 00000000000..3b8e0674320
--- /dev/null
+++ b/src/test/regress/expected/privs_in_db.out
@@ -0,0 +1,767 @@
+--
+-- Tests for database-specific role memberships.
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP ROLE IF EXISTS regress_priv_group1;
+DROP ROLE IF EXISTS regress_priv_group2;
+DROP DATABASE IF EXISTS regression_db_4;
+DROP DATABASE IF EXISTS regression_db_3;
+DROP DATABASE IF EXISTS regression_db_2;
+DROP DATABASE IF EXISTS regression_db_1;
+DROP ROLE IF EXISTS regress_role_granted;
+DROP ROLE IF EXISTS regress_role_read_34;
+DROP ROLE IF EXISTS regress_role_inherited_3;
+DROP ROLE IF EXISTS regress_role_inherited_34;
+DROP ROLE IF EXISTS regress_role_read_0;
+DROP ROLE IF EXISTS regress_role_read_12;
+DROP ROLE IF EXISTS regress_role_read_12_noinherit;
+DROP ROLE IF EXISTS regress_role_read_all_noinherit;
+DROP ROLE IF EXISTS regress_role_read_all_with_admin;
+DROP DATABASE IF EXISTS regression_db_0;
+DROP ROLE IF EXISTS regress_role_admin;
+RESET client_min_messages;
+-- test proper begins here
+CREATE ROLE regress_role_admin LOGIN CREATEROLE CREATEDB;
+GRANT pg_read_all_data TO regress_role_admin WITH ADMIN OPTION;
+GRANT pg_read_all_stats TO regress_role_admin WITH ADMIN OPTION;
+GRANT pg_maintain TO regress_role_admin WITH ADMIN OPTION;
+-- Populate test databases.
+CREATE DATABASE regression_db_0 OWNER regress_role_admin;
+\connect regression_db_0
+CREATE TABLE data AS SELECT generate_series(1, 3);
+CREATE VIEW regress_role_memberships
+ AS
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  CASE WHEN g.rolsuper THEN 'superuser' ELSE g.rolname END as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+JOIN pg_roles r ON r.oid = a.roleid
+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 'regress_role_%'
+ORDER BY
+  1, 2, 5, 3
+;
+CREATE DATABASE regression_db_1 TEMPLATE regression_db_0 OWNER regress_role_admin;
+CREATE DATABASE regression_db_2 TEMPLATE regression_db_1 OWNER regress_role_admin;
+CREATE DATABASE regression_db_3 TEMPLATE regression_db_1 OWNER regress_role_admin;
+CREATE DATABASE regression_db_4 TEMPLATE regression_db_1 OWNER regress_role_admin;
+SET SESSION AUTHORIZATION regress_role_admin;
+-- Read all cluster-wide with admin option
+CREATE ROLE regress_role_read_all_with_admin ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_all_with_admin WITH ADMIN OPTION;
+-- Read all in databases 1 and 2
+CREATE ROLE regress_role_read_12 ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_12 IN DATABASE regression_db_1;
+GRANT pg_read_all_data TO regress_role_read_12 IN DATABASE regression_db_2;
+-- Read all in databases 3 and 4 with admin option
+CREATE ROLE regress_role_read_34 ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_34 IN DATABASE regression_db_3 WITH ADMIN OPTION;
+GRANT pg_read_all_data TO regress_role_read_34 IN DATABASE regression_db_4 WITH ADMIN OPTION;
+-- Inherits read all in databases 3 and 4
+CREATE ROLE regress_role_inherited_34 ROLE regress_role_admin;
+GRANT regress_role_read_34 TO regress_role_inherited_34;
+-- Inherits read all in database 3
+CREATE ROLE regress_role_inherited_3 ROLE regress_role_admin;
+GRANT regress_role_read_34 TO regress_role_inherited_3 IN DATABASE regression_db_3;
+-- No inherit
+CREATE ROLE regress_role_read_all_noinherit NOINHERIT ROLE regress_role_admin;
+GRANT regress_role_read_all_with_admin TO regress_role_read_all_noinherit;
+-- No inherit in databases 1 and 2
+CREATE ROLE regress_role_read_12_noinherit NOINHERIT ROLE regress_role_admin;
+GRANT regress_role_read_12 TO regress_role_read_12_noinherit;
+-- Alternate syntax
+CREATE ROLE regress_role_read_0;
+GRANT pg_read_all_data TO regress_role_read_0, regress_role_read_all_noinherit IN CURRENT DATABASE;
+-- Failure due to missing database
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE non_existent; -- error
+ERROR:  database "non_existent" does not exist
+-- Should warn on duplicate grants
+GRANT pg_read_all_data TO regress_role_read_all_with_admin; -- notice
+NOTICE:  role "regress_role_read_all_with_admin" has already been granted membership in role "pg_read_all_data" by role "regress_role_admin"
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0; -- notice
+NOTICE:  role "regress_role_read_0" has already been granted membership in role "pg_read_all_data" in database "regression_db_0" by role "regress_role_admin"
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0 WITH ADMIN OPTION; -- notice
+NOTICE:  role "regress_role_read_0" has already been granted membership in role "pg_read_all_data" in database "regression_db_0" by role "regress_role_admin"
+GRANT pg_maintain TO regress_role_read_12 IN DATABASE regression_db_2;
+-- Cluster-wide role
+GRANT pg_read_all_stats TO regress_role_read_0;
+GRANT pg_read_all_stats TO regress_role_read_34 IN DATABASE regression_db_3;  -- makes no sense XXX
+-- Check membership table
+TABLE regress_role_memberships;
+               role               |              member              |      grantor       | admin_option |     datname     
+----------------------------------+----------------------------------+--------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser          | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser          | t            | 
+ pg_read_all_data                 | regress_role_read_0              | regress_role_admin | t            | regression_db_0
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_1
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser          | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin | f            | regression_db_3
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser          | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser          | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin | f            | 
+(32 rows)
+
+-- Test membership privileges (regression_db_1)
+\connect regression_db_1
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION regress_role_read_12;
+VACUUM data; -- error
+WARNING:  permission denied to vacuum "data", skipping it
+SET ROLE pg_read_all_data; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE regress_role_read_34; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE regress_role_read_34; -- error
+ERROR:  permission denied to set role "regress_role_read_34"
+SET SESSION AUTHORIZATION regress_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 regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+-- Test membership privileges (regression_db_2)
+\connect regression_db_2
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION regress_role_read_12;
+VACUUM data; -- success
+SET ROLE pg_read_all_data; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE regress_role_read_34; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE regress_role_read_34; -- error
+ERROR:  permission denied to set role "regress_role_read_34"
+SET SESSION AUTHORIZATION regress_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 regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12; -- success
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+-- Test membership privileges (regression_db_3)
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION regress_role_read_12;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+SET SESSION AUTHORIZATION regress_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 regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12; -- error
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+-- Test membership privileges (regression_db_4)
+\connect regression_db_4
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- success
+ generate_series 
+-----------------
+               1
+               2
+               3
+(3 rows)
+
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET SESSION AUTHORIZATION regress_role_read_12;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+ERROR:  permission denied to set role "pg_read_all_data"
+SET ROLE regress_role_read_34; -- error
+ERROR:  permission denied to set role "regress_role_read_34"
+SET SESSION AUTHORIZATION regress_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 regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+SET ROLE regress_role_read_12; -- error
+SELECT * FROM data; -- error
+ERROR:  permission denied for table data
+\connect regression_db_0
+-- Test cluster-wide role
+SET SESSION AUTHORIZATION regress_role_read_0;
+SELECT query FROM pg_stat_activity WHERE datname = 'regression_db_0';
+                                 query                                 
+-----------------------------------------------------------------------
+ SELECT query FROM pg_stat_activity WHERE datname = 'regression_db_0';
+(1 row)
+
+SET SESSION AUTHORIZATION regress_role_read_12;
+SELECT query FROM pg_stat_activity WHERE datname = 'regression_db_0';
+          query           
+--------------------------
+ <insufficient privilege>
+(1 row)
+
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_read_34;
+SELECT application_name, query FROM pg_stat_activity WHERE datname = 'regression_db_3';
+    application_name    |          query           
+------------------------+--------------------------
+ pg_regress/privs_in_db | <insufficient privilege>
+(1 row)
+
+\connect regression_db_0
+SET SESSION AUTHORIZATION regress_role_admin;
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- silent
+TABLE regress_role_memberships;
+               role               |              member              |      grantor       | admin_option |     datname     
+----------------------------------+----------------------------------+--------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser          | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser          | t            | 
+ pg_read_all_data                 | regress_role_read_0              | regress_role_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_1
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser          | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin | f            | regression_db_3
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser          | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser          | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin | f            | 
+(32 rows)
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- success
+REVOKE pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- warning
+WARNING:  role "regress_role_read_0" has not been granted membership in role "pg_read_all_data" by role "regress_role_admin"
+TABLE regress_role_memberships;
+               role               |              member              |      grantor       | admin_option |     datname     
+----------------------------------+----------------------------------+--------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser          | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser          | t            | 
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_1
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser          | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin | f            | regression_db_3
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser          | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser          | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin | f            | 
+(31 rows)
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM regress_role_read_12; -- warning
+TABLE regress_role_memberships;
+               role               |              member              |      grantor       | admin_option |     datname     
+----------------------------------+----------------------------------+--------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser          | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser          | t            | 
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser          | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin | f            | regression_db_3
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser          | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser          | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser          | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser          | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin | f            | 
+(30 rows)
+
+-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases
+CREATE ROLE regress_role_granted;
+SET SESSION AUTHORIZATION regress_role_read_all_with_admin;
+GRANT pg_read_all_data TO regress_role_granted; -- success
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_1; -- success
+GRANT regress_role_read_34 TO regress_role_granted; -- error
+ERROR:  permission denied to grant role "regress_role_read_34"
+DETAIL:  Only roles with the ADMIN option on role "regress_role_read_34" may grant this role.
+TABLE regress_role_memberships;
+               role               |              member              |             grantor              | admin_option |     datname     
+----------------------------------+----------------------------------+----------------------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser                        | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin               | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | regression_db_1
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | 
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin               | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin               | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin               | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin               | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin               | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin               | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin               | f            | regression_db_3
+ regress_role_granted             | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin               | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin               | f            | 
+(34 rows)
+
+-- Ensure database-specific admin option can only grant within that database
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- notice
+NOTICE:  role "regress_role_granted" has already been granted membership in role "pg_read_all_data" in database "regression_db_3" by role "regress_role_read_34"
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+\connect regression_db_4
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- error
+ERROR:  permission denied to grant role "pg_read_all_data"
+DETAIL:  Only roles with the ADMIN option on role "pg_read_all_data" may grant this role.
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- notice
+NOTICE:  role "regress_role_granted" has already been granted membership in role "pg_read_all_data" in database "regression_db_4" by role "regress_role_read_34"
+\connect regression_db_0
+SET SESSION AUTHORIZATION regress_role_admin;
+TABLE regress_role_memberships;
+               role               |              member              |             grantor              | admin_option |     datname     
+----------------------------------+----------------------------------+----------------------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser                        | t            | 
+ pg_maintain                      | regress_role_read_12             | regress_role_admin               | f            | regression_db_2
+ pg_read_all_data                 | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | regression_db_1
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_34             | f            | regression_db_3
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_34             | f            | regression_db_4
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | 
+ pg_read_all_data                 | regress_role_read_12             | regress_role_admin               | f            | regression_db_2
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin               | t            | regression_db_3
+ pg_read_all_data                 | regress_role_read_34             | regress_role_admin               | t            | regression_db_4
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin               | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin               | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin               | f            | 
+ pg_read_all_stats                | regress_role_read_34             | regress_role_admin               | f            | regression_db_3
+ regress_role_granted             | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_inherited_3         | regress_role_admin               | f            | regression_db_3
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin               | f            | 
+(36 rows)
+
+-- Should clean up the membership table when dropping a database
+DROP DATABASE regression_db_4;
+DROP DATABASE regression_db_3;
+DROP DATABASE regression_db_2;
+DROP DATABASE regression_db_1;
+TABLE regress_role_memberships;
+               role               |              member              |             grantor              | admin_option |     datname     
+----------------------------------+----------------------------------+----------------------------------+--------------+-----------------
+ pg_maintain                      | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_data                 | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | regression_db_0
+ pg_read_all_data                 | regress_role_granted             | regress_role_read_all_with_admin | f            | 
+ pg_read_all_data                 | regress_role_read_all_noinherit  | regress_role_admin               | f            | regression_db_0
+ pg_read_all_data                 | regress_role_read_all_with_admin | regress_role_admin               | t            | 
+ pg_read_all_stats                | regress_role_admin               | superuser                        | t            | 
+ pg_read_all_stats                | regress_role_read_0              | regress_role_admin               | f            | 
+ regress_role_granted             | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_3         | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_3         | regress_role_admin               | superuser                        | t            | 
+ regress_role_inherited_34        | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_inherited_34        | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_0              | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_12             | regress_role_read_12_noinherit   | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_12_noinherit   | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_34             | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_34             | regress_role_inherited_34        | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_noinherit  | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_admin               | regress_role_admin               | f            | 
+ regress_role_read_all_with_admin | regress_role_admin               | superuser                        | t            | 
+ regress_role_read_all_with_admin | regress_role_read_all_noinherit  | regress_role_admin               | f            | 
+(27 rows)
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE regress_role_granted;  -- dependency of 'regress_role_read_34'
+DROP ROLE regress_role_read_34;
+DROP ROLE regress_role_inherited_3;
+DROP ROLE regress_role_inherited_34;
+DROP ROLE regress_role_read_0;
+DROP ROLE regress_role_read_12;
+DROP ROLE regress_role_read_12_noinherit;
+DROP ROLE regress_role_read_all_noinherit;
+DROP ROLE regress_role_read_all_with_admin;
+RESET SESSION AUTHORIZATION;
+DROP OWNED BY regress_role_admin CASCADE;
+TABLE regress_role_memberships;
+       role        |       member       |  grantor  | admin_option | datname 
+-------------------+--------------------+-----------+--------------+---------
+ pg_maintain       | regress_role_admin | superuser | t            | 
+ pg_read_all_data  | regress_role_admin | superuser | t            | 
+ pg_read_all_stats | regress_role_admin | superuser | t            | 
+(3 rows)
+
+\connect template1
+DROP DATABASE regression_db_0;
+DROP ROLE regress_role_admin;
+SELECT datname FROM pg_database WHERE datname LIKE 'regression_db_%';
+ datname 
+---------
+(0 rows)
+
+SELECT rolname FROM pg_roles WHERE rolname LIKE 'regress_role_%';
+ rolname 
+---------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4f38104ba01..35d5f67059a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -92,7 +92,7 @@ test: write_parallel
 test: vacuum_parallel
 
 # no relation related tests can be put in this group
-test: publication subscription
+test: publication subscription privs_in_db
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/privs_in_db.sql b/src/test/regress/sql/privs_in_db.sql
new file mode 100644
index 00000000000..c2c009d6c5f
--- /dev/null
+++ b/src/test/regress/sql/privs_in_db.sql
@@ -0,0 +1,370 @@
+--
+-- Tests for database-specific role memberships.
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP ROLE IF EXISTS regress_priv_group1;
+DROP ROLE IF EXISTS regress_priv_group2;
+
+DROP DATABASE IF EXISTS regression_db_4;
+DROP DATABASE IF EXISTS regression_db_3;
+DROP DATABASE IF EXISTS regression_db_2;
+DROP DATABASE IF EXISTS regression_db_1;
+DROP ROLE IF EXISTS regress_role_granted;
+DROP ROLE IF EXISTS regress_role_read_34;
+DROP ROLE IF EXISTS regress_role_inherited_3;
+DROP ROLE IF EXISTS regress_role_inherited_34;
+DROP ROLE IF EXISTS regress_role_read_0;
+DROP ROLE IF EXISTS regress_role_read_12;
+DROP ROLE IF EXISTS regress_role_read_12_noinherit;
+DROP ROLE IF EXISTS regress_role_read_all_noinherit;
+DROP ROLE IF EXISTS regress_role_read_all_with_admin;
+DROP DATABASE IF EXISTS regression_db_0;
+DROP ROLE IF EXISTS regress_role_admin;
+
+RESET client_min_messages;
+
+-- test proper begins here
+
+CREATE ROLE regress_role_admin LOGIN CREATEROLE CREATEDB;
+GRANT pg_read_all_data TO regress_role_admin WITH ADMIN OPTION;
+GRANT pg_read_all_stats TO regress_role_admin WITH ADMIN OPTION;
+GRANT pg_maintain TO regress_role_admin WITH ADMIN OPTION;
+
+-- Populate test databases.
+CREATE DATABASE regression_db_0 OWNER regress_role_admin;
+
+\connect regression_db_0
+CREATE TABLE data AS SELECT generate_series(1, 3);
+
+CREATE VIEW regress_role_memberships
+ AS
+SELECT
+  r.rolname as role,
+  m.rolname as member,
+  CASE WHEN g.rolsuper THEN 'superuser' ELSE g.rolname END as grantor,
+  admin_option,
+  d.datname
+FROM pg_auth_members a
+JOIN pg_roles r ON r.oid = a.roleid
+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 'regress_role_%'
+ORDER BY
+  1, 2, 5, 3
+;
+
+CREATE DATABASE regression_db_1 TEMPLATE regression_db_0 OWNER regress_role_admin;
+CREATE DATABASE regression_db_2 TEMPLATE regression_db_1 OWNER regress_role_admin;
+CREATE DATABASE regression_db_3 TEMPLATE regression_db_1 OWNER regress_role_admin;
+CREATE DATABASE regression_db_4 TEMPLATE regression_db_1 OWNER regress_role_admin;
+
+SET SESSION AUTHORIZATION regress_role_admin;
+
+-- Read all cluster-wide with admin option
+CREATE ROLE regress_role_read_all_with_admin ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_all_with_admin WITH ADMIN OPTION;
+
+-- Read all in databases 1 and 2
+CREATE ROLE regress_role_read_12 ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_12 IN DATABASE regression_db_1;
+GRANT pg_read_all_data TO regress_role_read_12 IN DATABASE regression_db_2;
+
+-- Read all in databases 3 and 4 with admin option
+CREATE ROLE regress_role_read_34 ROLE regress_role_admin;
+GRANT pg_read_all_data TO regress_role_read_34 IN DATABASE regression_db_3 WITH ADMIN OPTION;
+GRANT pg_read_all_data TO regress_role_read_34 IN DATABASE regression_db_4 WITH ADMIN OPTION;
+
+-- Inherits read all in databases 3 and 4
+CREATE ROLE regress_role_inherited_34 ROLE regress_role_admin;
+GRANT regress_role_read_34 TO regress_role_inherited_34;
+
+-- Inherits read all in database 3
+CREATE ROLE regress_role_inherited_3 ROLE regress_role_admin;
+GRANT regress_role_read_34 TO regress_role_inherited_3 IN DATABASE regression_db_3;
+
+-- No inherit
+CREATE ROLE regress_role_read_all_noinherit NOINHERIT ROLE regress_role_admin;
+GRANT regress_role_read_all_with_admin TO regress_role_read_all_noinherit;
+
+-- No inherit in databases 1 and 2
+CREATE ROLE regress_role_read_12_noinherit NOINHERIT ROLE regress_role_admin;
+GRANT regress_role_read_12 TO regress_role_read_12_noinherit;
+
+-- Alternate syntax
+CREATE ROLE regress_role_read_0;
+GRANT pg_read_all_data TO regress_role_read_0, regress_role_read_all_noinherit IN CURRENT DATABASE;
+
+-- Failure due to missing database
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE non_existent; -- error
+
+-- Should warn on duplicate grants
+GRANT pg_read_all_data TO regress_role_read_all_with_admin; -- notice
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0; -- notice
+
+-- Should not warn if adjusting admin option
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0 WITH ADMIN OPTION; -- silent
+GRANT pg_read_all_data TO regress_role_read_0 IN DATABASE regression_db_0 WITH ADMIN OPTION; -- notice
+
+GRANT pg_maintain TO regress_role_read_12 IN DATABASE regression_db_2;
+
+-- Cluster-wide role
+GRANT pg_read_all_stats TO regress_role_read_0;
+GRANT pg_read_all_stats TO regress_role_read_34 IN DATABASE regression_db_3;  -- makes no sense XXX
+
+-- Check membership table
+TABLE regress_role_memberships;
+
+-- Test membership privileges (regression_db_1)
+\connect regression_db_1
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- error
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- error
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_12;
+VACUUM data; -- error
+SET ROLE pg_read_all_data; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+SET ROLE regress_role_read_34; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE regress_role_read_34; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12; -- success
+SELECT * FROM data; -- success
+
+-- Test membership privileges (regression_db_2)
+\connect regression_db_2
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- error
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- error
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_12;
+VACUUM data; -- success
+SET ROLE pg_read_all_data; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- error
+SET ROLE regress_role_read_34; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE regress_role_read_34; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12; -- success
+SELECT * FROM data; -- success
+
+-- Test membership privileges (regression_db_3)
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- success
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- success
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_12;
+SET ROLE pg_read_all_data; -- error
+
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+
+SET SESSION AUTHORIZATION regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12; -- error
+SELECT * FROM data; -- error
+
+-- Test membership privileges (regression_db_4)
+\connect regression_db_4
+SET SESSION AUTHORIZATION regress_role_admin;
+SET ROLE regress_role_read_all_with_admin;
+SELECT * FROM data; -- success
+SET ROLE regress_role_read_12;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_34;
+SELECT * FROM data; -- success
+SET ROLE regress_role_inherited_34;
+SELECT * FROM data; -- success
+SET ROLE regress_role_inherited_3;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_12;
+SET ROLE pg_read_all_data; -- error
+
+SET SESSION AUTHORIZATION regress_role_inherited_34;
+SET ROLE pg_read_all_data; -- success
+SET ROLE regress_role_read_34; -- success
+
+SET SESSION AUTHORIZATION regress_role_inherited_3;
+SET ROLE pg_read_all_data; -- error
+SET ROLE regress_role_read_34; -- error
+
+SET SESSION AUTHORIZATION regress_role_read_all_noinherit;
+SELECT * FROM data; -- error
+SET ROLE pg_read_all_data; -- success
+SELECT * FROM data; -- success
+
+SET SESSION AUTHORIZATION regress_role_read_12_noinherit;
+SELECT * FROM data; -- error
+SET ROLE regress_role_read_12; -- error
+SELECT * FROM data; -- error
+
+\connect regression_db_0
+
+-- Test cluster-wide role
+SET SESSION AUTHORIZATION regress_role_read_0;
+SELECT query FROM pg_stat_activity WHERE datname = 'regression_db_0';
+SET SESSION AUTHORIZATION regress_role_read_12;
+SELECT query FROM pg_stat_activity WHERE datname = 'regression_db_0';
+
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_read_34;
+SELECT application_name, query FROM pg_stat_activity WHERE datname = 'regression_db_3';
+
+\connect regression_db_0
+SET SESSION AUTHORIZATION regress_role_admin;
+
+-- Should not warn if revoking admin option
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- silent
+REVOKE ADMIN OPTION FOR pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- silent
+TABLE regress_role_memberships;
+
+-- Should warn if revoking a non-existent membership
+REVOKE pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- success
+REVOKE pg_read_all_data FROM regress_role_read_0 IN DATABASE regression_db_0; -- warning
+TABLE regress_role_memberships;
+
+-- Revoke should only apply to the specified level
+REVOKE pg_read_all_data FROM regress_role_read_12; -- warning
+TABLE regress_role_memberships;
+
+-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases
+CREATE ROLE regress_role_granted;
+SET SESSION AUTHORIZATION regress_role_read_all_with_admin;
+GRANT pg_read_all_data TO regress_role_granted; -- success
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_1; -- success
+GRANT regress_role_read_34 TO regress_role_granted; -- error
+TABLE regress_role_memberships;
+
+-- Ensure database-specific admin option can only grant within that database
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- error
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- error
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- error
+
+\connect regression_db_3
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- notice
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- error
+
+\connect regression_db_4
+SET SESSION AUTHORIZATION regress_role_read_34;
+GRANT pg_read_all_data TO regress_role_granted; -- error
+GRANT pg_read_all_data TO regress_role_granted IN CURRENT DATABASE; -- success
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_3; -- error
+GRANT pg_read_all_data TO regress_role_granted IN DATABASE regression_db_4; -- notice
+
+\connect regression_db_0
+SET SESSION AUTHORIZATION regress_role_admin;
+TABLE regress_role_memberships;
+
+-- Should clean up the membership table when dropping a database
+DROP DATABASE regression_db_4;
+DROP DATABASE regression_db_3;
+DROP DATABASE regression_db_2;
+DROP DATABASE regression_db_1;
+TABLE regress_role_memberships;
+
+-- Should clean up the membership table when dropping a role
+DROP ROLE regress_role_granted;  -- dependency of 'regress_role_read_34'
+DROP ROLE regress_role_read_34;
+DROP ROLE regress_role_inherited_3;
+DROP ROLE regress_role_inherited_34;
+DROP ROLE regress_role_read_0;
+DROP ROLE regress_role_read_12;
+DROP ROLE regress_role_read_12_noinherit;
+DROP ROLE regress_role_read_all_noinherit;
+DROP ROLE regress_role_read_all_with_admin;
+
+RESET SESSION AUTHORIZATION;
+DROP OWNED BY regress_role_admin CASCADE;
+TABLE regress_role_memberships;
+\connect template1
+DROP DATABASE regression_db_0;
+DROP ROLE regress_role_admin;
+SELECT datname FROM pg_database WHERE datname LIKE 'regression_db_%';
+SELECT rolname FROM pg_roles WHERE rolname LIKE 'regress_role_%';

base-commit: 4572d59e3c9b0ef6c056544b6336561efd9bb5ec
-- 
2.39.5