aboutsummaryrefslogtreecommitdiff
path: root/src/backend/commands/user.c
diff options
context:
space:
mode:
authorRobert Haas <rhaas@postgresql.org>2022-08-22 11:35:17 -0400
committerRobert Haas <rhaas@postgresql.org>2022-08-22 11:35:17 -0400
commitce6b672e4455820a0348214be0da1a024c3f619f (patch)
tree97d6a9dd5d89d4b3e7d4c1b4a0866f78e2ec2f11 /src/backend/commands/user.c
parent36f729e2bc3539e777cda698dc441b8ccec42142 (diff)
downloadpostgresql-ce6b672e4455820a0348214be0da1a024c3f619f.tar.gz
postgresql-ce6b672e4455820a0348214be0da1a024c3f619f.zip
Make role grant system more consistent with other privileges.
Previously, membership of role A in role B could be recorded in the catalog tables only once. This meant that a new grant of role A to role B would overwrite the previous grant. For other object types, a new grant of permission on an object - in this case role A - exists along side the existing grant provided that the grantor is different. Either grant can be revoked independently of the other, and permissions remain so long as at least one grant remains. Make role grants work similarly. Previously, when granting membership in a role, the superuser could specify any role whatsoever as the grantor, but for other object types, the grantor of record must be either the owner of the object, or a role that currently has privileges to perform a similar GRANT. Implement the same scheme for role grants, treating the bootstrap superuser as the role owner since roles do not have owners. This means that attempting to revoke a grant, or admin option on a grant, can now fail if there are dependent privileges, and that CASCADE can be used to revoke these. It also means that you can't grant ADMIN OPTION on a role back to a user who granted it directly or indirectly to you, similar to how you can't give WITH GRANT OPTION on a privilege back to a role which granted it directly or indirectly to you. Previously, only the superuser could specify GRANTED BY with a user other than the current user. Relax that rule to allow the grantor to be any role whose privileges the current user posseses. This doesn't improve compatibility with what we do for other object types, where support for GRANTED BY is entirely vestigial, but it makes this feature more usable and seems to make sense to change at the same time we're changing related behaviors. Along the way, fix "ALTER GROUP group_name ADD USER user_name" to require the same privileges as "GRANT group_name TO user_name". Previously, CREATEROLE privileges were sufficient for either, but only the former form was permissible with ADMIN OPTION on the role. Now, either CREATEROLE or ADMIN OPTION on the role suffices for either spelling. Patch by me, reviewed by Stephen Frost. Discussion: http://postgr.es/m/CA+TgmoaFr-RZeQ+WoQ5nKPv97oT9+aDgK_a5+qWHSgbDsMp1Vg@mail.gmail.com
Diffstat (limited to 'src/backend/commands/user.c')
-rw-r--r--src/backend/commands/user.c565
1 files changed, 498 insertions, 67 deletions
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index fc42b1cfd77..511ca8d8fd1 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -35,10 +35,32 @@
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/*
+ * Removing a role grant - or the admin option on it - might recurse to
+ * dependent grants. We use these values to reason about what would need to
+ * be done in such cases.
+ *
+ * RRG_NOOP indicates a grant that would not need to be altered by the
+ * operation.
+ *
+ * RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
+ * admin_option set to false by the operation.
+ *
+ * RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
+ * by the operation.
+ */
+typedef enum
+{
+ RRG_NOOP,
+ RRG_REMOVE_ADMIN_OPTION,
+ RRG_DELETE_GRANT
+} RevokeRoleGrantAction;
+
/* Potentially set by pg_upgrade_support functions */
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
@@ -54,7 +76,22 @@ static void AddRoleMems(const char *rolename, Oid roleid,
Oid grantorId, bool admin_opt);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt);
+ Oid grantorId, bool admin_opt, DropBehavior behavior);
+static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
+ bool is_grant);
+static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
+static bool plan_single_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
+static void plan_member_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions, Oid member);
+static void plan_recursive_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
/* Check if current user has createrole privileges */
@@ -449,7 +486,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ InvalidOid, false);
ReleaseSysCache(oldroletup);
}
@@ -461,10 +498,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ InvalidOid, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -624,7 +661,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
* property. Otherwise, if you don't have createrole, you're only allowed
- * to change your own password.
+ * to (1) change your own password or (2) add members to a role for which
+ * you have ADMIN OPTION.
*/
if (authform->rolsuper || dissuper)
{
@@ -649,12 +687,25 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
}
else if (!have_createrole_privilege())
{
- /* check the rest */
+ /* things you certainly can't do without CREATEROLE */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
- drolemembers || dvalidUntil || !dpassword || roleid != GetUserId())
+ dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
+
+ /* without CREATEROLE, can only change your own password */
+ if (dpassword && roleid != GetUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have CREATEROLE privilege to change another user's password")));
+
+ /* without CREATEROLE, can only add members to roles you admin */
+ if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
}
/* Convert validuntil to internal form */
@@ -805,11 +856,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT);
}
/*
@@ -1296,7 +1347,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->grantor)
grantor = get_rolespec_oid(stmt->grantor, false);
else
- grantor = GetUserId();
+ grantor = InvalidOid;
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
@@ -1330,7 +1381,7 @@ GrantRole(GrantRoleStmt *stmt)
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- stmt->admin_opt);
+ grantor, stmt->admin_opt, stmt->behavior);
}
/*
@@ -1431,7 +1482,7 @@ roleSpecsToIds(List *memberNames)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership
+ * grantorId: who is granting the membership (InvalidOid if not set explicitly)
* admin_opt: granting admin option?
*/
static void
@@ -1443,6 +1494,7 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1464,7 +1516,7 @@ AddRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -1483,29 +1535,25 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check integrity constraints like the lack of circular ADMIN OPTION
+ * grants without fear of race conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ /* Preliminary sanity checks. */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- HeapTuple tuple;
- Datum new_record[Natts_pg_auth_members] = {0};
- bool new_record_nulls[Natts_pg_auth_members] = {0};
- bool new_record_repl[Natts_pg_auth_members] = {0};
- Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1543,14 +1591,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership loops
+ * above, but it isn't. That's checking for role-member loop (e.g. A is a
+ * member of B and B is a member of A) while this is checking for a
+ * member-grantor loop (e.g. A gave ADMIN OPTION on X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION on
+ * X back to A).
+ */
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
+
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+
+ ReleaseSysCacheList(memlist);
+ }
+
+ /* Now perform the catalog updates. */
+ forboth(specitem, memberSpecs, iditem, memberIds)
+ {
+ RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
+ Oid memberid = lfirst_oid(iditem);
+ HeapTuple authmem_tuple;
+ HeapTuple tuple;
+ Datum new_record[Natts_pg_auth_members] = {0};
+ bool new_record_nulls[Natts_pg_auth_members] = {0};
+ bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* 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(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
+ ObjectIdGetDatum(memberid),
+ ObjectIdGetDatum(grantorId));
if (!HeapTupleIsValid(authmem_tuple))
{
authmem_form = NULL;
@@ -1562,8 +1690,9 @@ AddRoleMems(const char *rolename, Oid roleid,
if (!admin_opt || authmem_form->admin_option)
{
ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
ReleaseSysCache(authmem_tuple);
continue;
}
@@ -1577,28 +1706,12 @@ AddRoleMems(const char *rolename, Oid roleid,
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
- if (authmem_form->grantor != grantorId)
- {
- Oid *oldmembers = palloc(sizeof(Oid));
- Oid *newmembers = palloc(sizeof(Oid));
-
- /* updateAclDependencies wants to pfree array inputs */
- oldmembers[0] = authmem_form->grantor;
- newmembers[0] = grantorId;
-
- updateAclDependencies(AuthMemRelationId, authmem_form->oid,
- 0, InvalidOid,
- 1, oldmembers,
- 1, newmembers);
- }
-
ReleaseSysCache(authmem_tuple);
}
else
@@ -1637,17 +1750,23 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
+ * behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1669,40 +1788,69 @@ DelRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(GetUserId(), roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
rolename)));
}
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check for things like dependent privileges without fear of race
+ * conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+ actions = initialize_revoke_actions(memlist);
+
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RESTRICT was specified. plan_single_revoke() will figure
+ * out what to do with each catalog tuple.
+ */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- Form_pg_auth_members authmem_form;
- /*
- * Find entry for this role/member
- */
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
- ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
- if (!HeapTupleIsValid(authmem_tuple))
+ if (!plan_single_revoke(memlist, actions, memberid, grantorId,
+ admin_opt, behavior))
{
ereport(WARNING,
- (errmsg("role \"%s\" is not a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
continue;
}
+ }
+ /*
+ * We now know what to do with each catalog tuple: it should either be
+ * left alone, deleted, or just have the admin_option flag cleared.
+ * Perform the appropriate action in each case.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ if (actions[i] == RRG_NOOP)
+ continue;
+
+ authmem_tuple = &memlist->members[i]->tuple;
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
- if (!admin_opt)
+ if (actions[i] == RRG_DELETE_GRANT)
{
/*
* Remove the entry altogether, after first removing its
@@ -1729,15 +1877,298 @@ DelRoleMems(const char *rolename, Oid roleid,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
}
-
- ReleaseSysCache(authmem_tuple);
-
- /* CCI after each change, in case there are duplicates in list */
- CommandCounterIncrement();
}
+ ReleaseSysCacheList(memlist);
+
/*
* Close pg_authmem, but keep lock till commit.
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * If the grantor was not explicitly specified by the user, grantorId should
+ * be passed as InvalidOid, and this function will infer the user to be
+ * recorded as the grantor. In many cases, this will be the current user, but
+ * things get more complicated when the current user doesn't possess ADMIN
+ * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * on inheriting the privileges of a role which does have ADMIN OPTION. See
+ * below for details.
+ *
+ * If the grantor was specified by the user, then it must be a user that
+ * can legally be recorded as the grantor, as per the rule stated above.
+ * This is an integrity constraint, not a permissions check, and thus even
+ * superusers are subject to this restriction. However, there is also a
+ * permissions check: to specify a role as the grantor, the current user
+ * must possess the privileges of that role. Superusers will always pass
+ * this check, but for non-superusers it may lead to an error.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+{
+ /* If the grantor ID was not specified, pick one to use. */
+ if (!OidIsValid(grantorId))
+ {
+ /*
+ * Grants where the grantor is recorded as the bootstrap superuser do
+ * not depend on any other existing grants, so always default to this
+ * interpretation when possible.
+ */
+ if (has_createrole_privilege(currentUserId))
+ return BOOTSTRAP_SUPERUSERID;
+
+ /*
+ * Otherwise, the grantor must either have ADMIN OPTION on the role or
+ * inherit the privileges of a role which does. In the former case,
+ * record the grantor as the current user; in the latter, pick one of
+ * the roles that is "most directly" inherited by the current role
+ * (i.e. fewest "hops").
+ *
+ * (We shouldn't fail to find a best grantor, because we've already
+ * established that the current user has permission to perform the
+ * operation.)
+ */
+ grantorId = select_best_admin(currentUserId, roleid);
+ if (!OidIsValid(grantorId))
+ elog(ERROR, "no possible grantors");
+ return grantorId;
+ }
+
+ /*
+ * If an explicit grantor is specified, it must be a role whose privileges
+ * the current user possesses.
+ *
+ * It should also be a role that has ADMIN OPTION on the target role, but
+ * we check this condition only in case of GRANT. For REVOKE, no matching
+ * grant should exist anyway, but if it somehow does, let the user get rid
+ * of it.
+ */
+ if (is_grant)
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to grant privileges as role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+
+ if (grantorId != BOOTSTRAP_SUPERUSERID &&
+ select_best_admin(grantorId, roleid) != grantorId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("grantor must have ADMIN OPTION on \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+ else
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to revoke privileges granted by role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+ }
+
+ /*
+ * If a grantor was specified explicitly, always attribute the grant to
+ * that role (unless we error out above).
+ */
+ return grantorId;
+}
+
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * This constructs an array indicating that no actions are to be performed;
+ * that is, every element is initially RRG_NOOP.
+ */
+static RevokeRoleGrantAction *
+initialize_revoke_actions(CatCList *memlist)
+{
+ RevokeRoleGrantAction *result;
+ int i;
+
+ if (memlist->n_members == 0)
+ return NULL;
+
+ result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
+ for (i = 0; i < memlist->n_members; i++)
+ result[i] = RRG_NOOP;
+ return result;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke a grant, or just the
+ * admin option on a grant, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ *
+ * If behavior is DROP_RESTRICT, an error will occur if there are dependent
+ * role membership grants; if DROP_CASCADE, those grants will be scheduled
+ * for deletion.
+ *
+ * The return value is true if the matching grant was found in the list,
+ * and false if not.
+ */
+static bool
+plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor, bool revoke_admin_option_only,
+ DropBehavior behavior)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member &&
+ authmem_form->grantor == grantor)
+ {
+ plan_recursive_revoke(memlist, actions, i,
+ revoke_admin_option_only, behavior);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke all grants to
+ * a given member, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ */
+static void
+plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member)
+ plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
+ }
+}
+
+/*
+ * Workhorse for figuring out recursive revocation of role grants.
+ *
+ * This is similar to what recursive_revoke() does for ACLs.
+ */
+static void
+plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only, DropBehavior behavior)
+{
+ bool would_still_have_admin_option = false;
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+ int i;
+
+ /* If it's already been done, we can just return. */
+ if (actions[index] == RRG_DELETE_GRANT)
+ return;
+ if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
+ revoke_admin_option_only)
+ return;
+
+ /* Locate tuple data. */
+ authmem_tuple = &memlist->members[index]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ /*
+ * If the existing tuple does not have admin_option set, then we do not
+ * need to recurse. If we're just supposed to clear that bit we don't need
+ * to do anything at all; if we're supposed to remove the grant, we need
+ * to do something, but only to the tuple, and not any others.
+ */
+ if (!revoke_admin_option_only)
+ {
+ actions[index] = RRG_DELETE_GRANT;
+ if (!authmem_form->admin_option)
+ return;
+ }
+ else
+ {
+ if (!authmem_form->admin_option)
+ return;
+ actions[index] = RRG_REMOVE_ADMIN_OPTION;
+ }
+
+ /* Determine whether the member would still have ADMIN OPTION. */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->member == authmem_form->member &&
+ am_cascade_form->admin_option && actions[i] == RRG_NOOP)
+ {
+ would_still_have_admin_option = true;
+ break;
+ }
+ }
+
+ /* If the member would still have ADMIN OPTION, we need not recurse. */
+ if (would_still_have_admin_option)
+ return;
+
+ /*
+ * Recurse to grants that are not yet slated for deletion which have this
+ * member as the grantor.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->grantor == authmem_form->member &&
+ actions[i] != RRG_DELETE_GRANT)
+ {
+ if (behavior == DROP_RESTRICT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("dependent privileges exist"),
+ errhint("Use CASCADE to revoke them too.")));
+
+ plan_recursive_revoke(memlist, actions, i, false, behavior);
+ }
+ }
+}