v4-0003-Give-role-owners-control-over-owned-roles.patch

application/octet-stream

Filename: v4-0003-Give-role-owners-control-over-owned-roles.patch
Type: application/octet-stream
Part: 2
Message: Re: CREATEROLE and role ownership hierarchies

Patch

Format: format-patch
Series: patch v4-0003
Subject: Give role owners control over owned roles
File+
src/backend/catalog/aclchk.c 5 46
src/backend/catalog/objectaddress.c 3 19
src/backend/commands/schemacmds.c 1 1
src/backend/commands/user.c 21 7
src/backend/utils/adt/acl.c 118 0
src/include/utils/acl.h 1 0
src/test/modules/dummy_seclabel/expected/dummy_seclabel.out 8 4
src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql 9 3
src/test/regress/expected/create_role.out 23 45
src/test/regress/sql/create_role.sql 18 26
From 1784a5b51d4dbebf99798b5832d92b0f585feb08 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 11:42:27 -0800
Subject: [PATCH v4 3/5] Give role owners control over owned roles

Create a role ownership hierarchy.  The previous commit added owners
to roles.  This goes further, making role ownership transitive.  If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C.  Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.

This is a preparatory patch for changing how CREATEROLE works.
---
 src/backend/catalog/aclchk.c                  |  51 +-------
 src/backend/catalog/objectaddress.c           |  22 +---
 src/backend/commands/schemacmds.c             |   2 +-
 src/backend/commands/user.c                   |  28 +++--
 src/backend/utils/adt/acl.c                   | 118 ++++++++++++++++++
 src/include/utils/acl.h                       |   1 +
 .../expected/dummy_seclabel.out               |  12 +-
 .../dummy_seclabel/sql/dummy_seclabel.sql     |  12 +-
 src/test/regress/expected/create_role.out     |  68 ++++------
 src/test/regress/sql/create_role.sql          |  44 +++----
 10 files changed, 207 insertions(+), 151 deletions(-)

diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ddd205d656..ef36fad700 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,20 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
 bool
 pg_role_ownercheck(Oid role_oid, Oid roleid)
 {
-	HeapTuple		tuple;
-	Form_pg_authid	authform;
-	Oid				owner_oid;
-
 	/* Superusers bypass all permission checking. */
 	if (superuser_arg(roleid))
 		return true;
 
-	/* Otherwise, look up the owner of the role */
-	tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
-	if (!HeapTupleIsValid(tuple))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("role with OID %u does not exist",
-						role_oid)));
-	authform = (Form_pg_authid) GETSTRUCT(tuple);
-	owner_oid = authform->rolowner;
-
-	/*
-	 * Roles must necessarily have owners.  Even the bootstrap user has an
-	 * owner.  (It owns itself).  Other roles must form a proper tree.
-	 */
-	if (!OidIsValid(owner_oid))
-		ereport(ERROR,
-				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg("role \"%s\" with OID %u has invalid owner",
-						authform->rolname.data, authform->oid)));
-	if (authform->oid != BOOTSTRAP_SUPERUSERID &&
-		authform->rolowner == authform->oid)
-		ereport(ERROR,
-				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg("role \"%s\" with OID %u owns itself",
-						authform->rolname.data, authform->oid)));
-	if (authform->oid == BOOTSTRAP_SUPERUSERID &&
-		authform->rolowner != BOOTSTRAP_SUPERUSERID)
-		ereport(ERROR,
-				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg("role \"%s\" with OID %u owned by role with OID %u",
-						authform->rolname.data, authform->oid,
-						authform->rolowner)));
-	ReleaseSysCache(tuple);
-
-	return (owner_oid == roleid);
+	/* Otherwise, check the role ownership hierarchy */
+	return is_owner_of_role_nosuper(roleid, role_oid);
 }
 
 /*
  * Check whether specified role has CREATEROLE privilege (or is a superuser)
  *
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on!	Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
  */
 bool
 has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2bae3fbb17..cc19409ca3 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 							   NameListToString(castNode(List, object)));
 			break;
 		case OBJECT_ROLE:
-
-			/*
-			 * We treat roles as being "owned" by those with CREATEROLE priv,
-			 * except that superusers are only owned by superusers.
-			 */
-			if (superuser_arg(address.objectId))
-			{
-				if (!superuser_arg(roleid))
-					ereport(ERROR,
-							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-							 errmsg("must be superuser")));
-			}
-			else
-			{
-				if (!has_createrole_privilege(roleid))
-					ereport(ERROR,
-							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-							 errmsg("must have CREATEROLE privilege")));
-			}
+			if (!pg_role_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   strVal(object));
 			break;
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 6c6ab9ee34..3447806756 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
 		/*
 		 * must have create-schema rights
 		 *
-		 * NOTE: This is different from other alter-owner checks in that the
+		 * NOTE: This is different from most other alter-owner checks in that the
 		 * current user is checked for create privileges instead of the
 		 * destination owner.  This is consistent with the CREATE case for
 		 * schemas.  Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 14820744bf..11d5dffc90 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 			  !rolemembers &&
 			  !validUntil &&
 			  dpassword &&
-			  roleid == GetUserId()))
+			  !pg_role_ownercheck(roleid, GetUserId())))
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied")));
@@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 		}
 		else
 		{
-			if (!have_createrole_privilege() && roleid != GetUserId())
+			if (!have_createrole_privilege() &&
+				!pg_role_ownercheck(roleid, GetUserId()))
 				ereport(ERROR,
 						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 						 errmsg("permission denied")));
@@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt)
 				pg_auth_members_rel;
 	ListCell   *item;
 
-	if (!have_createrole_privilege())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("permission denied to drop role")));
-
 	/*
 	 * Scan the pg_authid relation to find the Oid of the role(s) to be
 	 * deleted.
@@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt)
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("must be superuser to drop superusers")));
 
+		if (!have_createrole_privilege() &&
+			!pg_role_ownercheck(roleid, GetUserId()))
+			ereport(ERROR,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("permission denied to drop role")));
+
 		/* DROP hook for the role being removed */
 		InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
 
@@ -1811,6 +1813,18 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("role may not own itself")));
 
+		/*
+		 * Must not create cycles in the role ownership hierarchy.  If this
+		 * role owns (directly or indirectly) the proposed new owner, disallow
+		 * the ownership transfer.
+		 */
+		if (is_owner_of_role_nosuper(authForm->oid, newOwnerId))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("role \"%s\" may not both own and be owned by role \"%s\"",
+							NameStr(authForm->rolname),
+							GetUserNameFromId(newOwnerId, false))));
+
 		authForm->rolowner = newOwnerId;
 		CatalogTupleUpdate(rel, &tup->t_self, tup);
 
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434..bcb7a50642 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4832,6 +4832,111 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
 }
 
 
+/*
+ * Get a list of roles which own the given role, directly or indirectly.
+ *
+ * Each role has only one direct owner.  The returned list contains the given
+ * role's owner, that role's owner, etc., up to the top of the ownership
+ * hierarchy, which is always the bootstrap superuser.
+ *
+ * Raises an error if any role ownership invariant is violated.  Returns NIL if
+ * the given roleid is invalid.
+ */
+static List *
+roles_is_owned_by(Oid roleid)
+{
+	List	   *owners_list = NIL;
+	Oid			role_oid = roleid;
+
+	/*
+	 * Start with the current role and follow the ownership chain upwards until
+	 * we reach the bootstrap superuser.  To defend against getting into an
+	 * infinite loop, we must check for ownership cycles.  We choose to perform
+	 * other corruption checks on the ownership structure while iterating, too.
+	 */
+	while (OidIsValid(role_oid))
+	{
+		HeapTuple		tuple;
+		Form_pg_authid	authform;
+		Oid				owner_oid;
+
+		/* Find the owner of the current iteration's role */
+		tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+		if (!HeapTupleIsValid(tuple))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("role with OID %u does not exist", role_oid)));
+
+		authform = (Form_pg_authid) GETSTRUCT(tuple);
+		owner_oid = authform->rolowner;
+
+		/*
+		 * Roles must necessarily have owners.  Even the bootstrap user has an
+		 * owner.  (It owns itself).
+		 */
+		if (!OidIsValid(owner_oid))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("role \"%s\" with OID %u has invalid owner",
+							NameStr(authform->rolname), authform->oid)));
+
+		/* The bootstrap user must own itself */
+		if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+			owner_oid != BOOTSTRAP_SUPERUSERID)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("role \"%s\" with OID %u owned by role with OID %u",
+							NameStr(authform->rolname), authform->oid,
+							authform->rolowner)));
+
+		/*
+		 * Roles other than the bootstrap user must not be their own direct
+		 * owners.
+		 */
+		if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+			authform->oid == owner_oid)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("role \"%s\" with OID %u owns itself",
+							NameStr(authform->rolname), authform->oid)));
+
+		ReleaseSysCache(tuple);
+
+		/* If we have reached the bootstrap user, we're done. */
+		if (role_oid == BOOTSTRAP_SUPERUSERID)
+		{
+			if (!owners_list)
+				owners_list = lappend_oid(owners_list, owner_oid);
+			break;
+		}
+
+		/*
+		 * For all other users, check they do not own themselves indirectly
+		 * through an ownership cycle.
+		 *
+		 * Scanning the list each time through this loop results in overall
+		 * quadratic work in the depth of the ownership chain, but we're
+		 * not on a critical performance path, nor do we expect ownership
+		 * hierarchies to be deep.
+		 */
+		if (owners_list && list_member_oid(owners_list,
+										   ObjectIdGetDatum(owner_oid)))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("role \"%s\" with OID %u indirectly owns itself",
+							GetUserNameFromId(owner_oid, false),
+							owner_oid)));
+
+		/* Done with sanity checks.  Add this owner to the list. */
+		owners_list = lappend_oid(owners_list, owner_oid);
+
+		/* Otherwise, iterate on this iteration's owner_oid. */
+		role_oid = owner_oid;
+	}
+
+	return owners_list;
+}
+
 /*
  * Does member have the privileges of role (directly or indirectly)?
  *
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
 	if (superuser_arg(member))
 		return true;
 
+	/* Owners of roles have every privilge the owned role has */
+	if (pg_role_ownercheck(role, member))
+		return true;
+
 	/*
 	 * Find all the roles that member has the privileges of, including
 	 * multi-level recursion, then see if target role is any one of them.
@@ -4921,6 +5030,15 @@ is_member_of_role_nosuper(Oid member, Oid role)
 						   role);
 }
 
+/*
+ * Is owner a direct or indirect owner of the role, not considering
+ * superuserness?
+ */
+bool
+is_owner_of_role_nosuper(Oid owner, Oid role)
+{
+	return list_member_oid(roles_is_owned_by(role), owner);
+}
 
 /*
  * Is member an admin of role?	That is, is member the role itself (subject to
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec9d480d67..7f20aaa9f2 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,6 +209,7 @@ 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_owner_of_role_nosuper(Oid owner, Oid role);
 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/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
 DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
 DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
 RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
 CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
 CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
 CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
 CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
 --
 -- Test of SECURITY LABEL statement with a plugin
 --
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
 SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified';			-- OK
 SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified';		-- OK
 SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified';	-- fail
@@ -29,6 +32,7 @@ ERROR:  '...invalid label...' is not a valid security label
 SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified';	-- OK
 SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified';	-- fail
 ERROR:  security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
 SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified';	-- fail (not owner)
 ERROR:  must be owner of table dummy_seclabel_tbl2
 SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret';		-- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified';			-- OK
 --
 -- Test for shared database object
 --
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
 SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified';			-- OK
 SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...';	-- fail
 ERROR:  '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified';	-- fail (
 ERROR:  role "regress_dummy_seclabel_user3" does not exist
 SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
 SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified';	-- fail (not privileged)
-ERROR:  must have CREATEROLE privilege
+ERROR:  must be owner of role regress_dummy_seclabel_user2
 RESET SESSION AUTHORIZATION;
 --
 -- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
 
 RESET client_min_messages;
 
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
 CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
 
 CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
 CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
 --
 -- Test of SECURITY LABEL statement with a plugin
 --
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
 
 SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified';			-- OK
 SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified';		-- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified';	-- fail
 SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...';	-- fail
 SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified';	-- OK
 SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified';	-- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
 SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified';	-- fail (not owner)
 SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret';		-- fail (not superuser)
 SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified';	-- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified';			-- OK
 --
 -- Test for shared database object
 --
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
 
 SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified';			-- OK
 SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...';	-- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 7a5b830de7..b6cc68eed5 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -58,8 +58,9 @@ CREATE ROLE regress_role_9 INHERIT;
 CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
 CREATE ROLE regress_role_11 PASSWORD NULL;
 CREATE ROLE regress_role_12
-  CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
-  IN ROLE regress_role_6, regress_role_7, regress_role_8;
+  CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+  IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
 \du+ regress_role_6
                                     List of roles
    Role name    |     Owner      |       Attributes        | Member of | Description 
@@ -98,11 +99,11 @@ CREATE ROLE regress_role_12
  regress_role_11 | regress_role_1 | Cannot login | {}        | 
 
 \du+ regress_role_12
-                                                      List of roles
-    Role name    |     Owner      |       Attributes       |                   Member of                    | Description 
------------------+----------------+------------------------+------------------------------------------------+-------------
- regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} | 
-                 |                | 2 connections          |                                                | 
+                                                         List of roles
+    Role name    |     Owner      |              Attributes              |            Member of            |    Description     
+-----------------+----------------+--------------------------------------+---------------------------------+--------------------
+ regress_role_12 | regress_role_1 | Create role, Create DB, Cannot login+| {regress_role_6,regress_role_7} | no login test role
+                 |                | 2 connections                        |                                 | 
 
 -- ok, backwards compatible noise words should be ignored
 CREATE ROLE regress_role_13 SYSID 12345;
@@ -143,21 +144,18 @@ CREATE TABLE regress_tbl_22 (i integer);
 CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
 CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
 REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
 SET SESSION AUTHORIZATION regress_role_7;
 DROP INDEX regress_idx_22;
-ERROR:  must be owner of index regress_idx_22
 ALTER TABLE regress_tbl_22 ADD COLUMN t text;
-ERROR:  must be owner of table regress_tbl_22
 DROP TABLE regress_tbl_22;
-ERROR:  must be owner of table regress_tbl_22
+-- fail, not a member of target role
 ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-ERROR:  must be owner of view regress_view_22
+ERROR:  must be member of role "regress_role_1"
+-- ok
 DROP VIEW regress_view_22;
-ERROR:  must be owner of view regress_view_22
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
 REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-ERROR:  permission denied to reassign objects
 -- ok, having CREATEROLE is enough to create roles in privileged roles
 CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
 CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
@@ -169,14 +167,14 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
 CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
 CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
 CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
-SET SESSION AUTHORIZATION regress_role_1;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-ERROR:  must be owner of role regress_role_20
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-ERROR:  permission denied to reassign objects
--- superuser can do it, though
+-- fail, cannot create ownership cycles
 RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_1 TO regress_role_22;
+ERROR:  role "regress_role_7" may not both own and be owned by role "regress_role_22"
+ALTER ROLE regress_role_1 OWNER TO regress_role_22;
+ERROR:  role "regress_role_1" may not both own and be owned by role "regress_role_22"
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_1;
 ALTER ROLE regress_role_20 OWNER TO regress_role_1;
 REASSIGN OWNED BY regress_role_20 TO regress_role_1;
 -- ok, superuser roles can drop superuser roles they own
@@ -187,14 +185,6 @@ SET SESSION AUTHORIZATION regress_role_1;
 DROP ROLE regress_role_3;
 DROP ROLE regress_role_4;
 DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-ERROR:  role "regress_role_14" does not exist
-DROP ROLE regress_role_15;
-ERROR:  role "regress_role_15" does not exist
-DROP ROLE regress_role_17;
-ERROR:  role "regress_role_17" does not exist
-DROP ROLE regress_role_19;
-ERROR:  role "regress_role_19" does not exist
 DROP ROLE regress_role_20;
 -- fail, cannot drop roles that own other roles
 DROP ROLE regress_role_7;
@@ -222,6 +212,7 @@ DROP ROLE regress_role_13;
 DROP ROLE regress_role_16;
 DROP ROLE regress_role_18;
 DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
 DROP ROLE regress_role_23;
 DROP ROLE regress_role_24;
 DROP ROLE regress_role_25;
@@ -232,28 +223,15 @@ DROP ROLE regress_role_29;
 DROP ROLE regress_role_30;
 DROP ROLE regress_role_31;
 DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-ERROR:  role "regress_role_22" cannot be dropped because some objects depend on it
-DETAIL:  owner of table regress_tbl_22
-owner of view regress_view_22
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-ERROR:  role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL:  owner of role regress_role_22
 -- fail, cannot drop ourself nor superusers
 DROP ROLE regress_role_super;
 ERROR:  must be superuser to drop superusers
 DROP ROLE regress_role_1;
 ERROR:  current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
 DROP ROLE regress_role_7;
 -- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
 DROP ROLE regress_role_1;
 ERROR:  role "regress_role_1" cannot be dropped because some objects depend on it
 DETAIL:  privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 71fe01ad5d..4a6f7840a2 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -39,8 +39,9 @@ CREATE ROLE regress_role_9 INHERIT;
 CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
 CREATE ROLE regress_role_11 PASSWORD NULL;
 CREATE ROLE regress_role_12
-  CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
-  IN ROLE regress_role_6, regress_role_7, regress_role_8;
+  CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+  IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
 
 \du+ regress_role_6
 \du+ regress_role_7
@@ -95,15 +96,19 @@ CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
 CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
 REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
 
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
 SET SESSION AUTHORIZATION regress_role_7;
 DROP INDEX regress_idx_22;
 ALTER TABLE regress_tbl_22 ADD COLUMN t text;
 DROP TABLE regress_tbl_22;
+
+-- fail, not a member of target role
 ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+
+-- ok
 DROP VIEW regress_view_22;
 
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
 REASSIGN OWNED BY regress_role_22 TO regress_role_7;
 
 -- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -118,13 +123,13 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
 CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
 CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
 
--- fail, cannot take ownership of these objects from regress_role_7
-SET SESSION AUTHORIZATION regress_role_1;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-
--- superuser can do it, though
+-- fail, cannot create ownership cycles
 RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_1 TO regress_role_22;
+ALTER ROLE regress_role_1 OWNER TO regress_role_22;
+
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_1;
 ALTER ROLE regress_role_20 OWNER TO regress_role_1;
 REASSIGN OWNED BY regress_role_20 TO regress_role_1;
 
@@ -137,10 +142,6 @@ SET SESSION AUTHORIZATION regress_role_1;
 DROP ROLE regress_role_3;
 DROP ROLE regress_role_4;
 DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-DROP ROLE regress_role_15;
-DROP ROLE regress_role_17;
-DROP ROLE regress_role_19;
 DROP ROLE regress_role_20;
 
 -- fail, cannot drop roles that own other roles
@@ -157,6 +158,7 @@ DROP ROLE regress_role_13;
 DROP ROLE regress_role_16;
 DROP ROLE regress_role_18;
 DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
 DROP ROLE regress_role_23;
 DROP ROLE regress_role_24;
 DROP ROLE regress_role_25;
@@ -168,25 +170,15 @@ DROP ROLE regress_role_30;
 DROP ROLE regress_role_31;
 DROP ROLE regress_role_32;
 
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-
 -- fail, cannot drop ourself nor superusers
 DROP ROLE regress_role_super;
 DROP ROLE regress_role_1;
 
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
 DROP ROLE regress_role_7;
 
 -- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
 DROP ROLE regress_role_1;
 
 -- ok, can drop role if we revoke privileges first
-- 
2.21.1 (Apple Git-122.3)