Ensure that pg_auth_members.grantor is always valid.
authorRobert Haas <[email protected]>
Thu, 18 Aug 2022 17:13:02 +0000 (13:13 -0400)
committerRobert Haas <[email protected]>
Thu, 18 Aug 2022 17:13:02 +0000 (13:13 -0400)
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, just as it does for other kinds of
privileges. "REASSIGN OWNED BY" will not, again just like what we do
in other cases involving privileges.

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.

A follow-on patch is planned with the goal of more thoroughly
rationalizing the behavior of role grants. This patch is just trying
to do enough to make sure that the data we store in the catalogs is at
some basic level valid.

Patch by me, reviewed by Stephen Frost

Discussion: http://postgr.es/m/CA+TgmoaFr-RZeQ+WoQ5nKPv97oT9+aDgK_a5+qWHSgbDsMp1Vg@mail.gmail.com

16 files changed:
doc/src/sgml/catalogs.sgml
src/backend/catalog/catalog.c
src/backend/catalog/dependency.c
src/backend/catalog/objectaddress.c
src/backend/catalog/pg_shdepend.c
src/backend/catalog/system_views.sql
src/backend/commands/alter.c
src/backend/commands/event_trigger.c
src/backend/commands/tablecmds.c
src/backend/commands/user.c
src/include/catalog/dependency.h
src/include/catalog/pg_auth_members.h
src/test/regress/expected/create_role.out
src/test/regress/expected/privileges.out
src/test/regress/sql/create_role.sql
src/test/regress/sql/privileges.sql

index ff0ed65772f084be044b49cbb79609e56934c171..2ce539aaf04a13aaccbf2293e4ca69510b1252c8 100644 (file)
@@ -1669,6 +1669,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <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>
index 6f43870779f6441de380948d6bf390cb7b56ee99..2abd6b007a248a59c1afdadde05d0c585bda8a43 100644 (file)
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
        relationId == AuthIdRolnameIndexId ||
        relationId == AuthMemMemRoleIndexId ||
        relationId == AuthMemRoleMemIndexId ||
+       relationId == AuthMemOidIndexId ||
+       relationId == AuthMemGrantorIndexId ||
        relationId == DatabaseNameIndexId ||
        relationId == DatabaseOidIndexId ||
        relationId == DbRoleSettingDatidRolidIndexId ||
index e119674b1f2749a75c39f646ce28ee85eda3000e..39768fa22be15d93205a2be4c1a002c2a3d1c784 100644 (file)
@@ -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"
@@ -172,6 +173,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 */
@@ -1502,6 +1504,7 @@ doDeletion(const ObjectAddress *object, int flags)
        case OCLASS_DEFACL:
        case OCLASS_EVENT_TRIGGER:
        case OCLASS_TRANSFORM:
+       case OCLASS_ROLE_MEMBERSHIP:
            DropObjectById(object);
            break;
 
@@ -1529,9 +1532,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)
@@ -1549,6 +1551,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 */
@@ -2914,6 +2919,9 @@ getObjectClass(const ObjectAddress *object)
        case AuthIdRelationId:
            return OCLASS_ROLE;
 
+       case AuthMemRelationId:
+           return OCLASS_ROLE_MEMBERSHIP;
+
        case DatabaseRelationId:
            return OCLASS_DATABASE;
 
index dab375f2b26752153765bdfc7d2e91a0b6e16156..798c1a2d1e08b833a53cd3de750cb665bf98009d 100644 (file)
@@ -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
@@ -3644,6 +3663,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;
@@ -4533,6 +4594,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;
@@ -5476,6 +5541,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;
index 3e8fa008b9dca959c3cb3e9177664938603dab0d..f2f227f887b4e6a8b97ecd381089ce1d469b0ff8 100644 (file)
@@ -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"
@@ -1364,11 +1365,6 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
                case SHARED_DEPENDENCY_INVALID:
                    elog(ERROR, "unexpected dependency type");
                    break;
-               case SHARED_DEPENDENCY_ACL:
-                   RemoveRoleFromObjectACL(roleid,
-                                           sdepForm->classid,
-                                           sdepForm->objid);
-                   break;
                case SHARED_DEPENDENCY_POLICY:
 
                    /*
@@ -1398,22 +1394,37 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
                        add_exact_object_address(&obj, deleteobjs);
                    }
                    break;
+               case SHARED_DEPENDENCY_ACL:
+
+                   /*
+                    * Dependencies on role grants are recorded using
+                    * SHARED_DEPENDENCY_ACL, but unlike a regular ACL list
+                    * which stores all permissions for a particular object in
+                    * a single ACL array, there's a separate catalog row for
+                    * each grant - so removing the grant just means removing
+                    * the entire row.
+                    */
+                   if (sdepForm->classid != AuthMemRelationId)
+                   {
+                       RemoveRoleFromObjectACL(roleid,
+                                               sdepForm->classid,
+                                               sdepForm->objid);
+                       break;
+                   }
+                   /* FALLTHROUGH */
                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;
            }
        }
index f369b1fc141ccbaf20fc3c6ca8426f702b12eca4..5a844b63a1c6ca76384a152ccaba4a452876cae0 100644 (file)
@@ -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;
 
index 5456b8222ba7ff6492d132c0f8de98ae093594b7..55219bb0974ad5e2e5c861d2e3805758950b2b2b 100644 (file)
@@ -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:
index 9f8fee7c1682b48273e202c8dcceb224ced70cc6..635d05405e144f812aa9ac7a93067cebe99d5a61 100644 (file)
@@ -1015,6 +1015,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;
index 8d7c68b8b3cbf013ad3c0859a2c2023a2992d5c5..9be04c8a1e7386115c1ab725148d610f804590bc 100644 (file)
@@ -12667,6 +12667,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:
index 94135fdd6b6a6cca21c279d065996e08d690a1b3..fc42b1cfd771691b00fdb9aa0cc9b254fed51633 100644 (file)
@@ -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 preliminary 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,31 @@ 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, 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.
+        *
+        * 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.
         *
-        * XXX what about grantor entries?  Maybe we should do one heap scan.
+        * 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 +1027,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 +1047,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 +1067,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 +1505,7 @@ AddRoleMems(const char *rolename, Oid roleid,
        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
@@ -1488,15 +1551,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 */
@@ -1513,13 +1583,41 @@ 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)
+           {
+               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
        {
+           Oid         objectId;
+           Oid        *newmembers = palloc(sizeof(Oid));
+
+           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);
+
+           /* updateAclDependencies wants to pfree array inputs */
+           newmembers[0] = grantorId;
+           updateAclDependencies(AuthMemRelationId, objectId,
+                                 0, InvalidOid,
+                                 0, NULL,
+                                 1, newmembers);
        }
 
        /* CCI after each change, in case there are duplicates in list */
@@ -1586,6 +1684,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
@@ -1601,9 +1700,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
+            * dependencies
+            */
+           deleteSharedDependencyRecordsFor(AuthMemRelationId,
+                                            authmem_form->oid, 0);
            CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
        }
        else
index 6684933dacfd9f4d78f466f2cf0db9c3fbb5d206..729c4c46c0d3a4c3bacac1962d1593ac516aaf70 100644 (file)
@@ -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 */
index 1bc027f133d50976c9b494044b05e4ab240834f7..c9d7697730c41daacd7f2cded22e7d59b1aa0f88 100644 (file)
@@ -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 */
index 4e67d7276031510c98eb8b78e263c1c9e721f683..c2465d0f4927903f914ac5b5bd711e09710387e6 100644 (file)
@@ -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:  privileges for membership of role regress_read_all_data in role pg_read_all_data
+privileges for membership of role regress_write_all_data in role pg_write_all_data
+privileges for membership of role regress_monitor in role pg_monitor
+privileges for membership of role regress_read_all_settings in role pg_read_all_settings
+privileges for membership of role regress_read_all_stats in role pg_read_all_stats
+privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+privileges for membership of role regress_read_server_files in role pg_read_server_files
+privileges for membership of role regress_write_server_files in role pg_write_server_files
+privileges for membership of role regress_execute_server_program in role pg_execute_server_program
+privileges for 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
index e10dd6f9ae5a171e18e2b801795901dbe55c991f..65b4a22ebc5360632c09b94a9f6fb616f7a40a0b 100644 (file)
@@ -33,6 +33,38 @@ 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 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+ERROR:  role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL:  privileges for membership of role regress_priv_user3 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+ERROR:  role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL:  privileges for membership of role regress_priv_user3 in role regress_priv_user1
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+ERROR:  role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL:  privileges for membership of role regress_priv_user4 in role regress_priv_user1
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+ERROR:  role "regress_priv_user5" cannot be dropped because some objects depend on it
+DETAIL:  privileges for membership of role regress_priv_user6 in role regress_priv_user1
+DROP ROLE regress_priv_user1, regress_priv_user5; -- 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;
 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;
index 292dc087975860f0957a3b1275270bc7154c772b..b696628238abfed34731b2a9d68b902b7d3d7d93 100644 (file)
@@ -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;
 
index 6d1fd3391a208717aecd77292a4d88251e684e95..66834e32a7e254247874584b8b08141899681eb9 100644 (file)
@@ -37,6 +37,33 @@ 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 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+DROP ROLE regress_priv_user1, regress_priv_user5; -- 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;
+
 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;