aboutsummaryrefslogtreecommitdiff
path: root/src/backend/commands/user.c
diff options
context:
space:
mode:
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);
+ }
+ }
+}