Proposal: allow database-specific role memberships
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}
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.
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
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.
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.
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
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 infact
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 problemput
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><iteration count></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><iteration count></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}
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
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 testedThe 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><iteration count></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><iteration count></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}
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/
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><iteration count></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><iteration count></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}
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.
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.log1 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.rejCould 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><iteration count></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><iteration count></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}
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.
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><iteration count></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><iteration count></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}
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?
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 opento
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:596I 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><iteration count></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><iteration count></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}
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
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
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#L1480Marked 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><iteration count></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><iteration count></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}
Hi all,
cfbot is once again green as of the v7 patch:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/37/3374
- Kenaniah
Patch doesn't apply again...
[image: 1jfj7m.jpg]
Attachments:
1jfj7m.jpgimage/jpeg; name=1jfj7m.jpgDownload
���� JFIF �� �
�� ��"