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$<iteration count>:&l
- Because user identities are cluster-wide,
- pg_auth_members
- is shared across all databases of a cluster: there is only one
- copy of pg_auth_members 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
+ dbid column). The pg_auth_members
+ catalog is shared across all databases of a cluster.
@@ -1703,6 +1702,17 @@ SCRAM-SHA-256$<iteration count>:&l
roleid to others
+
+
+
+ dbidoid
+ (references pg_database.oid)
+
+
+ ID of the database that this membership is constrained to; zero if membership is cluster-wide
+
+
+
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 role_specification ]
GRANT role_name [, ...] TO role_specification [, ...]
+ [ IN DATABASE database_name | IN CURRENT DATABASE ]
[ WITH ADMIN OPTION ]
[ GRANTED BY role_specification ]
@@ -243,7 +244,23 @@ GRANT role_name [, ...] TO GRANT 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.
+
+
+
+ If IN DATABASE database_name
+ is specified, membership in role_name
+ will be effective only when the recipient is connected to the database specified by
+ database_name.
+
+
+
+ If IN CURRENT DATABASE is specified, the membership in
+ role_name will be effective only when the
+ recipient is connected to the same database that the grant was issued in.
@@ -269,6 +286,10 @@ GRANT role_name [, ...] TO GROUP
in role_specification.
+
+
+ See for more information about role memberships.
+
@@ -390,10 +411,18 @@ GRANT ALL PRIVILEGES ON kinds TO manuel;
- Grant membership in role admins to user joe:
+ Grant cluster-wide membership in role admins to user joe:
GRANT admins TO joe;
+
+
+
+ Grant read and write access to user alice in the database
+ named sales:
+
+
+GRANT pg_read_all_data, pg_write_all_data TO alice IN DATABASE sales;
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 ]
role_name [, ...] FROM role_specification [, ...]
+ [ IN DATABASE database_name | IN CURRENT DATABASE ]
[ GRANTED BY role_specification ]
[ CASCADE | RESTRICT ]
@@ -298,6 +299,14 @@ REVOKE ALL PRIVILEGES ON kinds FROM manuel;
REVOKE admins FROM joe;
+
+
+
+ Revoke write access for user bob from the sales
+ database:
+
+
+REVOKE pg_write_all_data FROM bob IN DATABASE sales;
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 name;
GRANT group_role TO role1, ... ;
REVOKE group_role FROM role1, ... ;
+
+ Role membership can also be granted and revoked within the context of a specific database:
+
+GRANT group_role TO role1, ... IN CURRENT DATABASE;
+REVOKE group_role FROM role1, ... IN DATABASE database_name;
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;
SET ROLE admin.
-
-
-
To destroy a group role, use DROP ROLE:
@@ -639,7 +641,7 @@ DROP ROLE doomed_role;
- Administrators can grant access to these roles to users using the
+ Administrators can grant cluster-wide access to these roles to users using the
GRANT command, for example:
@@ -647,6 +649,14 @@ GRANT pg_signal_backend TO admin_user;
+
+ Access can also be granted within the context of a specific database,
+ for example:
+
+
+GRANT pg_read_all_data TO reporting_user IN DATABASE sales;
+
+
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 opt_type
%type foreign_server_version opt_foreign_server_version
-%type opt_in_database
+%type opt_in_database opt_grant_in_database
%type OptSchemaName
%type 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}