From 0bdf2a0f8fb3298ad1f67e2c1008d4bc156f1c5c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 24 Jun 2022 15:43:13 -0400
Subject: [PATCH v1] Ensure that pg_auth_members.grantor is always valid.

Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.

Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor.
"DROP OWNED BY" will remove the grant, and "REASSIGN OWNED BY"
will change the grantor, similar to what happens with permissions
on other types of SQL objects, where the grantor is regarded as the
"owner" of the grant.

pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
---
 doc/src/sgml/catalogs.sgml                |  10 ++
 src/backend/catalog/catalog.c             |   2 +
 src/backend/catalog/dependency.c          |  14 +-
 src/backend/catalog/objectaddress.c       | 108 +++++++++++++
 src/backend/catalog/pg_shdepend.c         |  25 ++-
 src/backend/catalog/system_views.sql      |   2 +-
 src/backend/commands/alter.c              |   1 +
 src/backend/commands/event_trigger.c      |   1 +
 src/backend/commands/tablecmds.c          |   1 +
 src/backend/commands/user.c               | 176 ++++++++++++++++------
 src/include/catalog/dependency.h          |   1 +
 src/include/catalog/pg_auth_members.h     |   5 +-
 src/test/regress/expected/create_role.out |  16 +-
 src/test/regress/expected/privileges.out  |  31 ++++
 src/test/regress/sql/create_role.sql      |   7 +-
 src/test/regress/sql/privileges.sql       |  26 ++++
 16 files changed, 363 insertions(+), 63 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 25b02c4e37..2683fb4cfe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index e784538aae..f5e894c0c6 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
 		relationId == AuthIdRolnameIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
+		relationId == AuthMemOidIndexId ||
+		relationId == AuthMemGrantorIndexId ||
 		relationId == DatabaseNameIndexId ||
 		relationId == DatabaseOidIndexId ||
 		relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index de10923391..5738c60b9e 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
@@ -171,6 +172,7 @@ static const Oid object_classes[] = {
 	TSTemplateRelationId,		/* OCLASS_TSTEMPLATE */
 	TSConfigRelationId,			/* OCLASS_TSCONFIG */
 	AuthIdRelationId,			/* OCLASS_ROLE */
+	AuthMemRelationId,			/* OCLASS_ROLE_MEMBERSHIP */
 	DatabaseRelationId,			/* OCLASS_DATABASE */
 	TableSpaceRelationId,		/* OCLASS_TBLSPACE */
 	ForeignDataWrapperRelationId,	/* OCLASS_FDW */
@@ -1493,6 +1495,7 @@ doDeletion(const ObjectAddress *object, int flags)
 		case OCLASS_TSPARSER:
 		case OCLASS_TSDICT:
 		case OCLASS_TSTEMPLATE:
+		case OCLASS_ROLE_MEMBERSHIP:
 		case OCLASS_FDW:
 		case OCLASS_FOREIGN_SERVER:
 		case OCLASS_USER_MAPPING:
@@ -1526,9 +1529,8 @@ doDeletion(const ObjectAddress *object, int flags)
  * Accepts the same flags as performDeletion (though currently only
  * PERFORM_DELETION_CONCURRENTLY does anything).
  *
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else.  Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
  */
 void
 AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1546,6 +1548,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
 		else
 			LockRelationOid(object->objectId, AccessExclusiveLock);
 	}
+	else if (object->classId == AuthMemRelationId)
+		LockSharedObject(object->classId, object->objectId, 0,
+						 AccessExclusiveLock);
 	else
 	{
 		/* assume we should lock the whole object not a sub-object */
@@ -2840,6 +2845,9 @@ getObjectClass(const ObjectAddress *object)
 		case AuthIdRelationId:
 			return OCLASS_ROLE;
 
+		case AuthMemRelationId:
+			return OCLASS_ROLE_MEMBERSHIP;
+
 		case DatabaseRelationId:
 			return OCLASS_DATABASE;
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8377b4f7d4..7c5fd23af7 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		-1,
 		true
 	},
+	{
+		"role membership",
+		AuthMemRelationId,
+		AuthMemOidIndexId,
+		-1,
+		-1,
+		Anum_pg_auth_members_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		Anum_pg_auth_members_grantor,
+		InvalidAttrNumber,
+		-1,
+		true
+	},
 	{
 		"rule",
 		RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
 	{
 		"role", OBJECT_ROLE
 	},
+	/* OCLASS_ROLE_MEMBERSHIP */
+	{
+		"role membership", -1	/* unmapped */
+	},
 	/* OCLASS_DATABASE */
 	{
 		"database", OBJECT_DATABASE
@@ -3648,6 +3667,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_ROLE_MEMBERSHIP:
+			{
+				Relation	amDesc;
+				ScanKeyData skey[1];
+				SysScanDesc rcscan;
+				HeapTuple	tup;
+				Form_pg_auth_members amForm;
+
+				amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+				ScanKeyInit(&skey[0],
+							Anum_pg_auth_members_oid,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(object->objectId));
+
+				rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+											NULL, 1, skey);
+
+				tup = systable_getnext(rcscan);
+
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "could not find tuple for role membership %u",
+							 object->objectId);
+
+					systable_endscan(rcscan);
+					table_close(amDesc, AccessShareLock);
+					break;
+				}
+
+				amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("membership of role %s in role %s"),
+								 GetUserNameFromId(amForm->member, false),
+								 GetUserNameFromId(amForm->roleid, false));
+
+				systable_endscan(rcscan);
+				table_close(amDesc, AccessShareLock);
+				break;
+			}
+
 		case OCLASS_DATABASE:
 			{
 				char	   *datname;
@@ -4537,6 +4598,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "role");
 			break;
 
+		case OCLASS_ROLE_MEMBERSHIP:
+			appendStringInfoString(&buffer, "role membership");
+			break;
+
 		case OCLASS_DATABASE:
 			appendStringInfoString(&buffer, "database");
 			break;
@@ -5480,6 +5545,49 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_ROLE_MEMBERSHIP:
+			{
+				Relation	authMemDesc;
+				ScanKeyData skey[1];
+				SysScanDesc amscan;
+				HeapTuple	tup;
+				Form_pg_auth_members amForm;
+
+				authMemDesc = table_open(AuthMemRelationId,
+										 AccessShareLock);
+
+				ScanKeyInit(&skey[0],
+							Anum_pg_auth_members_oid,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(object->objectId));
+
+				amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+											NULL, 1, skey);
+
+				tup = systable_getnext(amscan);
+
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+							 object->objectId);
+
+					systable_endscan(amscan);
+					table_close(authMemDesc, AccessShareLock);
+					break;
+				}
+
+				amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("membership of role %s in role %s"),
+								 GetUserNameFromId(amForm->member, false),
+								 GetUserNameFromId(amForm->roleid, false));
+
+				systable_endscan(amscan);
+				table_close(authMemDesc, AccessShareLock);
+				break;
+			}
+
 		case OCLASS_DATABASE:
 			{
 				char	   *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..c8c1f78567 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1399,21 +1400,18 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
 					}
 					break;
 				case SHARED_DEPENDENCY_OWNER:
-					/* If a local object, save it for deletion below */
-					if (sdepForm->dbid == MyDatabaseId)
+					/* Save it for deletion below */
+					obj.classId = sdepForm->classid;
+					obj.objectId = sdepForm->objid;
+					obj.objectSubId = sdepForm->objsubid;
+					/* as above */
+					AcquireDeletionLock(&obj, 0);
+					if (!systable_recheck_tuple(scan, tuple))
 					{
-						obj.classId = sdepForm->classid;
-						obj.objectId = sdepForm->objid;
-						obj.objectSubId = sdepForm->objsubid;
-						/* as above */
-						AcquireDeletionLock(&obj, 0);
-						if (!systable_recheck_tuple(scan, tuple))
-						{
-							ReleaseDeletionLock(&obj);
-							break;
-						}
-						add_exact_object_address(&obj, deleteobjs);
+						ReleaseDeletionLock(&obj);
+						break;
 					}
+					add_exact_object_address(&obj, deleteobjs);
 					break;
 			}
 		}
@@ -1593,6 +1591,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
 				case DatabaseRelationId:
 				case TSConfigRelationId:
 				case TSDictionaryRelationId:
+				case AuthMemRelationId:
 					{
 						Oid			classId = sdepForm->classid;
 						Relation	catalog;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index fedaed533b..eb95737fcc 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
     SELECT
         rolname AS groname,
         oid AS grosysid,
-        ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+        ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
     FROM pg_authid
     WHERE NOT rolcanlogin;
 
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case OCLASS_TRIGGER:
 		case OCLASS_SCHEMA:
 		case OCLASS_ROLE:
+		case OCLASS_ROLE_MEMBERSHIP:
 		case OCLASS_DATABASE:
 		case OCLASS_TBLSPACE:
 		case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 4642527881..d09cdc6c0a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1016,6 +1016,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_DATABASE:
 		case OCLASS_TBLSPACE:
 		case OCLASS_ROLE:
+		case OCLASS_ROLE_MEMBERSHIP:
 		case OCLASS_PARAMETER_ACL:
 			/* no support for global objects */
 			return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..aaf0876381 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12671,6 +12671,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TSTEMPLATE:
 			case OCLASS_TSCONFIG:
 			case OCLASS_ROLE:
+			case OCLASS_ROLE_MEMBERSHIP:
 			case OCLASS_DATABASE:
 			case OCLASS_TBLSPACE:
 			case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 984305ba31..6b1719193c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -911,6 +911,7 @@ DropRole(DropRoleStmt *stmt)
 	Relation	pg_authid_rel,
 				pg_auth_members_rel;
 	ListCell   *item;
+	List	   *role_addresses = NIL;
 
 	if (!have_createrole_privilege())
 		ereport(ERROR,
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
 
 	/*
 	 * Scan the pg_authid relation to find the Oid of the role(s) to be
-	 * deleted.
+	 * deleted and perform prleliminary permissions and sanity checks.
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -932,10 +933,9 @@ DropRole(DropRoleStmt *stmt)
 					tmp_tuple;
 		Form_pg_authid roleform;
 		ScanKeyData scankey;
-		char	   *detail;
-		char	   *detail_log;
 		SysScanDesc sscan;
 		Oid			roleid;
+		ObjectAddress  *role_address;
 
 		if (rolspec->roletype != ROLESPEC_CSTRING)
 			ereport(ERROR,
@@ -991,34 +991,32 @@ DropRole(DropRoleStmt *stmt)
 		/* DROP hook for the role being removed */
 		InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
 
+		/* Don't leak the syscache tuple */
+		ReleaseSysCache(tuple);
+
 		/*
 		 * Lock the role, so nobody can add dependencies to her while we drop
 		 * her.  We keep the lock until the end of transaction.
 		 */
 		LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
 
-		/* Check for pg_shdepend entries depending on this role */
-		if (checkSharedDependencies(AuthIdRelationId, roleid,
-									&detail, &detail_log))
-			ereport(ERROR,
-					(errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
-					 errmsg("role \"%s\" cannot be dropped because some objects depend on it",
-							role),
-					 errdetail_internal("%s", detail),
-					 errdetail_log("%s", detail_log)));
-
 		/*
-		 * Remove the role from the pg_authid table
-		 */
-		CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
-		ReleaseSysCache(tuple);
-
-		/*
-		 * Remove role from the pg_auth_members table.  We have to remove all
-		 * tuples that show it as either a role or a member.
+		 * If there is a pg_auth_members entry that has one of the roles to
+		 * be dropped as the roleid or member, it should be silently removed
+		 * without complaint, but if there is a pg_auth_members entry that has
+		 * one of the roles to be dropped as the grantor, the operation should
+		 * fail.
 		 *
-		 * XXX what about grantor entries?	Maybe we should do one heap scan.
+		 * It's possible, however, that a single pg_auth_members entry could
+		 * fall into multiple categories - e.g. the user could do "GRANT foo
+		 * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want
+		 * such an operation to succeed regardless of the order in which the
+		 * to-be-dropped roles are passed to DROP ROLE.
+		 *
+		 * To make that work, we remove all pg_auth_members entries that can
+		 * be silently removed in this loop, and then below we'll make a second
+		 * pass over the list of roles to be removed and check for any
+		 * remaining dependencies.
 		 */
 		ScanKeyInit(&scankey,
 					Anum_pg_auth_members_roleid,
@@ -1030,6 +1028,11 @@ DropRole(DropRoleStmt *stmt)
 
 		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
 		{
+			Form_pg_auth_members	authmem_form;
+
+			authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+			deleteSharedDependencyRecordsFor(AuthMemRelationId,
+											 authmem_form->oid, 0);
 			CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
 		}
 
@@ -1045,22 +1048,16 @@ DropRole(DropRoleStmt *stmt)
 
 		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
 		{
+			Form_pg_auth_members	authmem_form;
+
+			authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+			deleteSharedDependencyRecordsFor(AuthMemRelationId,
+											 authmem_form->oid, 0);
 			CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
 		}
 
 		systable_endscan(sscan);
 
-		/*
-		 * Remove any comments or security labels on this role.
-		 */
-		DeleteSharedComments(roleid, AuthIdRelationId);
-		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
-		/*
-		 * Remove settings for this role.
-		 */
-		DropSetting(InvalidOid, roleid);
-
 		/*
 		 * Advance command counter so that later iterations of this loop will
 		 * see the changes already made.  This is essential if, for example,
@@ -1071,6 +1068,72 @@ DropRole(DropRoleStmt *stmt)
 		 * itself.)
 		 */
 		CommandCounterIncrement();
+
+		/* Looks tentatively OK, add it to the list. */
+		role_address = palloc(sizeof(ObjectAddress));
+		role_address->classId = AuthIdRelationId;
+		role_address->objectId = roleid;
+		role_address->objectSubId = 0;
+		role_addresses = lappend(role_addresses, role_address);
+	}
+
+	/*
+	 * Second pass over the roles to be removed.
+	 */
+	foreach(item, role_addresses)
+	{
+		ObjectAddress *role_address = lfirst(item);
+		Oid			roleid = role_address->objectId;
+		HeapTuple	tuple;
+		Form_pg_authid roleform;
+		char	   *detail;
+		char	   *detail_log;
+
+		/*
+		 * Re-find the pg_authid tuple.
+		 *
+		 * Since we've taken a lock on the role OID, it shouldn't be possible
+		 * for the tuple to have been deleted -- or for that matter updated --
+		 * unless the user is manually modifying the system catalogs.
+		 */
+		tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "could not find tuple for role %u", roleid);
+		roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+		/*
+		 * Check for pg_shdepend entries depending on this role.
+		 *
+		 * This needs to happen after we've completed removing any
+		 * pg_auth_members entries that can be removed silently, in order
+		 * to avoid spurious failures. See notes above for more details.
+		 */
+		if (checkSharedDependencies(AuthIdRelationId, roleid,
+									&detail, &detail_log))
+			ereport(ERROR,
+					(errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+					 errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+							NameStr(roleform->rolname)),
+					 errdetail_internal("%s", detail),
+					 errdetail_log("%s", detail_log)));
+
+		/*
+		 * Remove the role from the pg_authid table
+		 */
+		CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+		ReleaseSysCache(tuple);
+
+		/*
+		 * Remove any comments or security labels on this role.
+		 */
+		DeleteSharedComments(roleid, AuthIdRelationId);
+		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+		/*
+		 * Remove settings for this role.
+		 */
+		DropSetting(InvalidOid, roleid);
 	}
 
 	/*
@@ -1443,6 +1506,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 		Datum		new_record[Natts_pg_auth_members];
 		bool		new_record_nulls[Natts_pg_auth_members];
 		bool		new_record_repl[Natts_pg_auth_members];
+		Form_pg_auth_members	authmem_form;
 
 		/*
 		 * pg_database_owner is never a role member.  Lifting this restriction
@@ -1488,15 +1552,22 @@ AddRoleMems(const char *rolename, Oid roleid,
 		authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
 										ObjectIdGetDatum(roleid),
 										ObjectIdGetDatum(memberid));
-		if (HeapTupleIsValid(authmem_tuple) &&
-			(!admin_opt ||
-			 ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+		if (!HeapTupleIsValid(authmem_tuple))
 		{
-			ereport(NOTICE,
-					(errmsg("role \"%s\" is already a member of role \"%s\"",
-							get_rolespec_name(memberRole), rolename)));
-			ReleaseSysCache(authmem_tuple);
-			continue;
+			authmem_form = NULL;
+		}
+		else
+		{
+			authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+			if (!admin_opt || authmem_form->admin_option)
+			{
+				ereport(NOTICE,
+						(errmsg("role \"%s\" is already a member of role \"%s\"",
+								get_rolespec_name(memberRole), rolename)));
+				ReleaseSysCache(authmem_tuple);
+				continue;
+			}
 		}
 
 		/* Build a tuple to insert or update */
@@ -1517,13 +1588,24 @@ AddRoleMems(const char *rolename, Oid roleid,
 									  new_record,
 									  new_record_nulls, new_record_repl);
 			CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+			if (authmem_form->grantor != grantorId)
+				changeDependencyOnOwner(AuthMemRelationId, authmem_form->oid,
+										grantorId);
+
 			ReleaseSysCache(authmem_tuple);
 		}
 		else
 		{
+			Oid		objectId;
+
+			objectId = GetNewObjectId();
+			new_record[Anum_pg_auth_members_oid - 1] = objectId;
 			tuple = heap_form_tuple(pg_authmem_dsc,
 									new_record, new_record_nulls);
 			CatalogTupleInsert(pg_authmem_rel, tuple);
+
+			recordDependencyOnOwner(AuthMemRelationId, objectId, grantorId);
 		}
 
 		/* CCI after each change, in case there are duplicates in list */
@@ -1590,6 +1672,7 @@ DelRoleMems(const char *rolename, Oid roleid,
 		RoleSpec   *memberRole = lfirst(specitem);
 		Oid			memberid = lfirst_oid(iditem);
 		HeapTuple	authmem_tuple;
+		Form_pg_auth_members	authmem_form;
 
 		/*
 		 * Find entry for this role/member
@@ -1605,9 +1688,16 @@ DelRoleMems(const char *rolename, Oid roleid,
 			continue;
 		}
 
+		authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
 		if (!admin_opt)
 		{
-			/* Remove the entry altogether */
+			/*
+			 * Remove the entry altogether, after first removing its
+			 * dependenceis
+			 */
+			deleteSharedDependencyRecordsFor(AuthMemRelationId,
+											 authmem_form->oid, 0);
 			CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
 		}
 		else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index d027075a4c..0a1e072bef 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
 	OCLASS_TSTEMPLATE,			/* pg_ts_template */
 	OCLASS_TSCONFIG,			/* pg_ts_config */
 	OCLASS_ROLE,				/* pg_authid */
+	OCLASS_ROLE_MEMBERSHIP,		/* pg_auth_members */
 	OCLASS_DATABASE,			/* pg_database */
 	OCLASS_TBLSPACE,			/* pg_tablespace */
 	OCLASS_FDW,					/* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
  */
 CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
 {
+	Oid			oid;			/* oid */
 	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
 	Oid			member BKI_LOOKUP(pg_authid);	/* ID of a member of that role */
 	Oid			grantor BKI_LOOKUP(pg_authid);	/* who granted the membership */
@@ -42,7 +43,9 @@ 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_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(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_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
 
 #endif							/* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..b6d1de26c9 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR:  role "regress_nosuch_recursive" does not exist
 DROP ROLE regress_nosuch_admin_recursive;
 ERROR:  role "regress_nosuch_admin_recursive" does not exist
 DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR:  role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL:  owner of membership of role regress_read_all_data in role pg_read_all_data
+owner of membership of role regress_write_all_data in role pg_write_all_data
+owner of membership of role regress_monitor in role pg_monitor
+owner of membership of role regress_read_all_settings in role pg_read_all_settings
+owner of membership of role regress_read_all_stats in role pg_read_all_stats
+owner of membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+owner of membership of role regress_read_server_files in role pg_read_server_files
+owner of membership of role regress_write_server_files in role pg_write_server_files
+owner of membership of role regress_execute_server_program in role pg_execute_server_program
+owner of membership of role regress_signal_backend in role pg_signal_backend
 -- ok, should be able to drop non-superuser roles we created
 DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
 DROP ROLE regress_login;
 DROP ROLE regress_inherit;
 DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
 DROP ROLE regress_write_server_files;
 DROP ROLE regress_execute_server_program;
 DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
 -- fail, role still owns database objects
 DROP ROLE regress_tenant;
 ERROR:  role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 03df567d50..c91783bc14 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,37 @@ CREATE USER regress_priv_user8;
 CREATE USER regress_priv_user9;
 CREATE USER regress_priv_user10;
 CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- fail, dependency
+ERROR:  role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL:  owner of membership of role regress_priv_user2 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user3 TO regress_priv_user4;
+DROP ROLE regress_priv_user3; -- ok now
+DROP ROLE regress_priv_user4; -- fail, dependency
+ERROR:  role "regress_priv_user4" cannot be dropped because some objects depend on it
+DETAIL:  owner of membership of role regress_priv_user2 in role regress_priv_user1
+DROP OWNED BY regress_priv_user4;
+DROP ROLE regress_priv_user4; -- ok now
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user5 TO regress_priv_user6 GRANTED BY regress_priv_user1;
+DROP ROLE regress_priv_user1; -- should fail, dependency
+ERROR:  role "regress_priv_user1" cannot be dropped because some objects depend on it
+DETAIL:  owner of membership of role regress_priv_user6 in role regress_priv_user5
+DROP ROLE regress_priv_user5; -- ok
+DROP ROLE regress_priv_user1; -- ok now
+GRANT regress_priv_user7 TO regress_priv_user6 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- should fail, dependency
+ERROR:  role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL:  owner of membership of role regress_priv_user6 in role regress_priv_user7
+DROP ROLE regress_priv_user2, regress_priv_user6; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+CREATE USER regress_priv_user6;
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
 GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
 DROP ROLE regress_nosuch_admin_recursive;
 DROP ROLE regress_plainrole;
 
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
 -- ok, should be able to drop non-superuser roles we created
 DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
 DROP ROLE regress_login;
 DROP ROLE regress_inherit;
 DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
 DROP ROLE regress_execute_server_program;
 DROP ROLE regress_signal_backend;
 
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
 -- fail, role still owns database objects
 DROP ROLE regress_tenant;
 
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 2a6ba38e52..e9a415d3d8 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
 CREATE USER regress_priv_user10;
 CREATE ROLE regress_priv_role;
 
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user3 TO regress_priv_user4;
+DROP ROLE regress_priv_user3; -- ok now
+DROP ROLE regress_priv_user4; -- fail, dependency
+DROP OWNED BY regress_priv_user4;
+DROP ROLE regress_priv_user4; -- ok now
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user5 TO regress_priv_user6 GRANTED BY regress_priv_user1;
+DROP ROLE regress_priv_user1; -- should fail, dependency
+DROP ROLE regress_priv_user5; -- ok
+DROP ROLE regress_priv_user1; -- ok now
+GRANT regress_priv_user7 TO regress_priv_user6 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- should fail, dependency
+DROP ROLE regress_priv_user2, regress_priv_user6; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+CREATE USER regress_priv_user6;
+
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
 GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
-- 
2.24.3 (Apple Git-128)

