Reintroduce MAINTAIN privilege and pg_maintain predefined role.
authorNathan Bossart
Wed, 13 Mar 2024 19:49:26 +0000 (14:49 -0500)
committerNathan Bossart
Wed, 13 Mar 2024 19:49:26 +0000 (14:49 -0500)
Roles with MAINTAIN on a relation may run VACUUM, ANALYZE, REINDEX,
REFRESH MATERIALIZE VIEW, CLUSTER, and LOCK TABLE on the relation.
Roles with privileges of pg_maintain may run those same commands on
all relations.

This was previously committed for v16, but it was reverted in
commit 151c22deee due to concerns about search_path tricks that
could be used to escalate privileges to the table owner.  Commits
2af07e2f7459825d1639, and c7ea3f4229 resolved these concerns by
restricting search_path when running maintenance commands.

Bumps catversion.

Reviewed-by: Jeff Davis
Discussion: https://postgr.es/m/20240305161235.GA3478007%40nathanxps13

42 files changed:
doc/src/sgml/ddl.sgml
doc/src/sgml/func.sgml
doc/src/sgml/ref/alter_default_privileges.sgml
doc/src/sgml/ref/analyze.sgml
doc/src/sgml/ref/cluster.sgml
doc/src/sgml/ref/grant.sgml
doc/src/sgml/ref/lock.sgml
doc/src/sgml/ref/refresh_materialized_view.sgml
doc/src/sgml/ref/reindex.sgml
doc/src/sgml/ref/revoke.sgml
doc/src/sgml/ref/vacuum.sgml
doc/src/sgml/user-manag.sgml
src/backend/catalog/aclchk.c
src/backend/commands/analyze.c
src/backend/commands/cluster.c
src/backend/commands/indexcmds.c
src/backend/commands/lockcmds.c
src/backend/commands/matview.c
src/backend/commands/tablecmds.c
src/backend/commands/vacuum.c
src/backend/postmaster/autovacuum.c
src/backend/utils/adt/acl.c
src/bin/pg_dump/dumputils.c
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/psql/tab-complete.c
src/include/catalog/catversion.h
src/include/catalog/pg_authid.dat
src/include/commands/tablecmds.h
src/include/commands/vacuum.h
src/include/nodes/parsenodes.h
src/include/utils/acl.h
src/test/isolation/expected/cluster-conflict-partition.out
src/test/isolation/specs/cluster-conflict-partition.spec
src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm
src/test/regress/expected/cluster.out
src/test/regress/expected/create_index.out
src/test/regress/expected/dependency.out
src/test/regress/expected/privileges.out
src/test/regress/expected/rowsecurity.out
src/test/regress/sql/cluster.sql
src/test/regress/sql/dependency.sql
src/test/regress/sql/privileges.sql

index 9d7e2c756be31d5403d96027b2ef73997f806416..8616a8e9cc9a5f75d7ae9586e35724e28249dd31 100644 (file)
@@ -1868,8 +1868,8 @@ ALTER TABLE products RENAME TO items;
    INSERTUPDATEDELETE,
    TRUNCATEREFERENCESTRIGGER,
    CREATECONNECTTEMPORARY,
-   EXECUTEUSAGESET
-   and ALTER SYSTEM.
+   EXECUTEUSAGESET,
+   ALTER SYSTEM, and MAINTAIN.
    The privileges applicable to a particular
    object vary depending on the object's type (table, function, etc.).
    More detail about the meanings of these privileges appears below.
@@ -2160,7 +2160,19 @@ REVOKE ALL ON accounts FROM PUBLIC;
       
      
     
-   
+
+   
+    MAINTAIN
+    
+     
+      Allows VACUUMANALYZE,
+      CLUSTERREFRESH MATERIALIZED VIEW,
+      REINDEX, and LOCK TABLE on a
+      relation.
+     
+    
+   
+  
 
    The privileges required by other commands are listed on the
    reference page of the respective command.
@@ -2309,6 +2321,11 @@ REVOKE ALL ON accounts FROM PUBLIC;
       A
       PARAMETER
      
+     
+      MAINTAIN
+      m
+      TABLE
+     
      
    
   
@@ -2399,7 +2416,7 @@ REVOKE ALL ON accounts FROM PUBLIC;
      
      
       TABLE (and table-like objects)
-      arwdDxt
+      arwdDxtm
       none
       \dp
      
@@ -2465,11 +2482,11 @@ GRANT SELECT (col1), UPDATE (col1) ON mytable TO miriam_rw;
 
 => \dp mytable
                                   Access privileges
- Schema |  Name   | Type  |   Access privileges   |   Column privileges   | Policies
---------+---------+-------+-----------------------+-----------------------+----------
- public | mytable | table | miriam=arwdDxt/miriam+| col1:                +|
-        |         |       | =r/miriam            +|   miriam_rw=rw/miriam |
-        |         |       | admin=arw/miriam      |                       |
+ Schema |  Name   | Type  |   Access privileges    |   Column privileges   | Policies
+--------+---------+-------+------------------------+-----------------------+----------
+ public | mytable | table | miriam=arwdDxtm/miriam+| col1:                +|
+        |         |       | =r/miriam             +|   miriam_rw=rw/miriam |
+        |         |       | admin=arw/miriam       |                       |
 (1 row)
 
   
index 0bb7aeb40ece72b246dfcb8143c1aad2ca46f7f4..91f1e69359495d0f7d65d7db95a41583540f7138 100644 (file)
@@ -24232,7 +24232,7 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute');
         are SELECTINSERT,
         UPDATEDELETE,
         TRUNCATEREFERENCES,
-        and TRIGGER.
+        TRIGGER, and MAINTAIN.
        
       
 
index 78744470c8d15f7f5ee1ea8f72d483f14519d42b..1de4c5c1b4e2cef6669fc286ee6ca45a7332aeaa 100644 (file)
@@ -28,7 +28,7 @@ ALTER DEFAULT PRIVILEGES
 
 where abbreviated_grant_or_revoke is one of:
 
-GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
+GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN }
     [, ...] | ALL [ PRIVILEGES ] }
     ON TABLES
     TO { [ GROUP ] role_name | PUBLIC } [, ...] [ WITH GRANT OPTION ]
@@ -51,7 +51,7 @@ GRANT { USAGE | CREATE | ALL [ PRIVILEGES ] }
     TO { [ GROUP ] role_name | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
 REVOKE [ GRANT OPTION FOR ]
-    { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
+    { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN }
     [, ...] | ALL [ PRIVILEGES ] }
     ON TABLES
     FROM { [ GROUP ] role_name | PUBLIC } [, ...]
index bad121a1f19b09c958eecc241af61d034c0bbb1b..2b94b378e9f315fd379e9a21eace1caa0763c7fa 100644 (file)
@@ -174,11 +174,9 @@ ANALYZE [ ( option [, ...] ) ] [ 
   Notes
 
   
-   To analyze a table, one must ordinarily be the table's owner or a
-   superuser.  However, database owners are allowed to
+   To analyze a table, one must ordinarily have the MAINTAIN
+   privilege on the table.  However, database owners are allowed to
    analyze all tables in their databases, except shared catalogs.
-   (The restriction for shared catalogs means that a true database-wide
-   ANALYZE can only be performed by a superuser.)
    ANALYZE will skip over any tables that the calling user
    does not have permission to analyze.
   
index 24340e7b9bf6df1666706ed78ef4dda19d3033ca..c5760244e67e2ca1801b44a65ee37581a8b4660a 100644 (file)
@@ -68,9 +68,8 @@ CLUSTER [ ( option [, ...] ) ] [ 
    CLUSTER without a
    table_name reclusters all the
    previously-clustered tables in the current database that the calling user
-   owns, or all such tables if called by a superuser.  This
-   form of CLUSTER cannot be executed inside a transaction
-   block.
+   has privileges for.  This form of CLUSTER cannot be
+   executed inside a transaction block.
   
 
   
@@ -131,6 +130,11 @@ CLUSTER [ ( option [, ...] ) ] [ 
  
   Notes
 
+   
+    To cluster a table, one must have the MAINTAIN privilege
+    on the table.
+   
+
    
     In cases where you are accessing single rows randomly
     within a table, the actual order of the data in the
index 9d27b7fcde5695ecd18e0bed88ecebc0c9835b0f..65b1fe77119583eb5695aec5b29419eff446d6c1 100644 (file)
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  
 
-GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
+GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN }
     [, ...] | ALL [ PRIVILEGES ] }
     ON { [ TABLE ] table_name [, ...]
          | ALL TABLES IN SCHEMA schema_name [, ...] }
@@ -193,6 +193,7 @@ GRANT role_name [, ...] TO 
      USAGE
      SET
      ALTER SYSTEM
+     MAINTAIN
      
       
        Specific types of privileges, as defined in .
index 6ce2518de74b8d5d7aaa355657606cc95294ce67..070855da18b2a5a4385917eb852662ee0b27fb87 100644 (file)
@@ -166,8 +166,8 @@ LOCK [ TABLE ] [ ONLY ] name [ * ]
 
    
     To lock a table, the user must have the right privilege for the specified
-    lockmode, or be the table's
-    owner or a superuser.  If the user has
+    lockmode.
+    If the user has MAINTAIN,
     UPDATEDELETE, or
     TRUNCATE privileges on the table, any 
     class="parameter">lockmode is permitted. If the user has
index e4e5145058f9fea23b7a0f7a32ea2f9a1e99cc37..8ed43ade803ec2bec20355c7c3f36aa53cd2fe97 100644 (file)
@@ -31,8 +31,9 @@ REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name
 
   
    REFRESH MATERIALIZED VIEW completely replaces the
-   contents of a materialized view.  To execute this command you must be the
-   owner of the materialized view.  The old contents are discarded.  If
+   contents of a materialized view.  To execute this command you must have the
+   MAINTAIN
+   privilege on the materialized view.  The old contents are discarded.  If
    WITH DATA is specified (or defaults) the backing query
    is executed to provide the new data, and the materialized view is left in a
    scannable state.  If WITH NO DATA is specified no new
index b214861742be0a1bc96a13803b07e0be16f8aa65..2942dccf1e2e198a5ff7d836cea62afbb7fb4914 100644 (file)
@@ -298,16 +298,21 @@ REINDEX [ ( option [, ...] ) ] { DA
   
 
   
-   Reindexing a single index or table requires being the owner of that
-   index or table.  Reindexing a schema or database requires being the
-   owner of that schema or database.  Note specifically that it's thus
+   Reindexing a single index or table requires
+   having the MAINTAIN privilege on the
+   table.  Note that while REINDEX on a partitioned index or
+   table requires having the MAINTAIN privilege on the
+   partitioned table, such commands skip the privilege checks when processing
+   the individual partitions.  Reindexing a schema or database requires being the
+   owner of that schema or database or having privileges of the
+   pg_maintain
+   role.  Note specifically that it's thus
    possible for non-superusers to rebuild indexes of tables owned by
-   other users.  However, as a special exception, when
-   REINDEX DATABASEREINDEX SCHEMA
-   or REINDEX SYSTEM is issued by a non-superuser,
-   indexes on shared catalogs will be skipped unless the user owns the
-   catalog (which typically won't be the case).  Of course, superusers
-   can always reindex anything.
+   other users.  However, as a special exception,
+   REINDEX DATABASEREINDEX SCHEMA,
+   and REINDEX SYSTEM will skip indexes on shared catalogs
+   unless the user has the MAINTAIN privilege on the
+   catalog.
   
 
   
index 2db66bbf378e22c4fa4f1989ead18468d1fd24e0..8df492281a1c519754da38197b0a865c8a30c94d 100644 (file)
@@ -22,7 +22,7 @@ PostgreSQL documentation
  
 
 REVOKE [ GRANT OPTION FOR ]
-    { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
+    { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN }
     [, ...] | ALL [ PRIVILEGES ] }
     ON { [ TABLE ] table_name [, ...]
          | ALL TABLES IN SCHEMA schema_name [, ...] }
index df0dd71a37081d35ac8949a9e7d6b2d49bf28a94..9857b35627b3ef9265e85e14e152e75dbf121d72 100644 (file)
@@ -434,11 +434,9 @@ VACUUM [ ( option [, ...] ) ] [ 
   Notes
 
    
-    To vacuum a table, one must ordinarily be the table's owner or a
-    superuser.  However, database owners are allowed to
+    To vacuum a table, one must ordinarily have the MAINTAIN
+    privilege on the table.  However, database owners are allowed to
     vacuum all tables in their databases, except shared catalogs.
-    (The restriction for shared catalogs means that a true database-wide
-    VACUUM can only be performed by a superuser.)
     VACUUM will skip over any tables that the calling user
     does not have permission to vacuum.
    
index e026a4e27178096e89752127eacb3bed155df53f..07a16247d76f15c2c11a4024365098b3343a6999 100644 (file)
@@ -682,6 +682,18 @@ DROP ROLE doomed_role;
        the CHECKPOINT
        command.
       
+      
+       pg_maintain
+       Allow executing
+       VACUUM,
+       ANALYZE,
+       CLUSTER,
+       REFRESH MATERIALIZED VIEW,
+       REINDEX,
+       and LOCK TABLE on all
+       relations, as if having MAINTAIN rights on those
+       objects, even without having it explicitly.
+      
       
        pg_use_reserved_connections
        Allow use of connection slots reserved via
index 023938682d9f14d5e373d1e8c4bb38dda6b7e0ce..7abf3c2a74a1d7476d2df74225e6d951fb427ac0 100644 (file)
@@ -2627,6 +2627,8 @@ string_to_privilege(const char *privname)
        return ACL_SET;
    if (strcmp(privname, "alter system") == 0)
        return ACL_ALTER_SYSTEM;
+   if (strcmp(privname, "maintain") == 0)
+       return ACL_MAINTAIN;
    if (strcmp(privname, "rule") == 0)
        return 0;               /* ignore old RULE privileges */
    ereport(ERROR,
@@ -2668,6 +2670,8 @@ privilege_to_string(AclMode privilege)
            return "SET";
        case ACL_ALTER_SYSTEM:
            return "ALTER SYSTEM";
+       case ACL_MAINTAIN:
+           return "MAINTAIN";
        default:
            elog(ERROR, "unrecognized privilege: %d", (int) privilege);
    }
@@ -3426,6 +3430,17 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
        has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA))
        result |= (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE));
 
+   /*
+    * Check if ACL_MAINTAIN is being checked and, if so, and not already set
+    * as part of the result, then check if the user is a member of the
+    * pg_maintain role, which allows VACUUM, ANALYZE, CLUSTER, REFRESH
+    * MATERIALIZED VIEW, and REINDEX on all relations.
+    */
+   if (mask & ACL_MAINTAIN &&
+       !(result & ACL_MAINTAIN) &&
+       has_privs_of_role(roleid, ROLE_PG_MAINTAIN))
+       result |= ACL_MAINTAIN;
+
    return result;
 }
 
index 29b6a20f1edcfd1b44cf0a0d956210d7502ed5fe..28880bd29912b07349c2a72d73690dd28b38f28a 100644 (file)
@@ -149,16 +149,15 @@ analyze_rel(Oid relid, RangeVar *relation,
        return;
 
    /*
-    * Check if relation needs to be skipped based on ownership.  This check
+    * Check if relation needs to be skipped based on privileges.  This check
     * happens also when building the relation list to analyze for a manual
     * operation, and needs to be done additionally here as ANALYZE could
-    * happen across multiple transactions where relation ownership could have
-    * changed in-between.  Make sure to generate only logs for ANALYZE in
-    * this case.
+    * happen across multiple transactions where privileges could have changed
+    * in-between.  Make sure to generate only logs for ANALYZE in this case.
     */
-   if (!vacuum_is_relation_owner(RelationGetRelid(onerel),
-                                 onerel->rd_rel,
-                                 params->options & VACOPT_ANALYZE))
+   if (!vacuum_is_permitted_for_relation(RelationGetRelid(onerel),
+                                         onerel->rd_rel,
+                                         params->options & ~VACOPT_VACUUM))
    {
        relation_close(onerel, ShareUpdateExclusiveLock);
        return;
index cea7c8ea368faa67c6bc41805810f5b8760a664d..c04886c4090ababde2f19584c2c79e8a8aafd4db 100644 (file)
@@ -77,6 +77,7 @@ static void copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
 static List *get_tables_to_cluster(MemoryContext cluster_context);
 static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
                                               Oid indexOid);
+static bool cluster_is_permitted_for_relation(Oid relid, Oid userid);
 
 
 /*---------------------------------------------------------------------------
@@ -144,7 +145,8 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel)
        tableOid = RangeVarGetRelidExtended(stmt->relation,
                                            AccessExclusiveLock,
                                            0,
-                                           RangeVarCallbackOwnsTable, NULL);
+                                           RangeVarCallbackMaintainsTable,
+                                           NULL);
        rel = table_open(tableOid, NoLock);
 
        /*
@@ -362,8 +364,8 @@ cluster_rel(Oid tableOid, Oid indexOid, ClusterParams *params)
     */
    if (recheck)
    {
-       /* Check that the user still owns the relation */
-       if (!object_ownercheck(RelationRelationId, tableOid, save_userid))
+       /* Check that the user still has privileges for the relation */
+       if (!cluster_is_permitted_for_relation(tableOid, save_userid))
        {
            relation_close(OldHeap, AccessExclusiveLock);
            goto out;
@@ -1619,7 +1621,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 
 
 /*
- * Get a list of tables that the current user owns and
+ * Get a list of tables that the current user has privileges on and
  * have indisclustered set.  Return the list in a List * of RelToCluster
  * (stored in the specified memory context), each one giving the tableOid
  * and the indexOid on which the table is already clustered.
@@ -1636,8 +1638,8 @@ get_tables_to_cluster(MemoryContext cluster_context)
    List       *rtcs = NIL;
 
    /*
-    * Get all indexes that have indisclustered set and are owned by
-    * appropriate user.
+    * Get all indexes that have indisclustered set and that the current user
+    * has the appropriate privileges for.
     */
    indRelation = table_open(IndexRelationId, AccessShareLock);
    ScanKeyInit(&entry,
@@ -1651,7 +1653,7 @@ get_tables_to_cluster(MemoryContext cluster_context)
 
        index = (Form_pg_index) GETSTRUCT(indexTuple);
 
-       if (!object_ownercheck(RelationRelationId, index->indrelid, GetUserId()))
+       if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId()))
            continue;
 
        /* Use a permanent memory context for the result list */
@@ -1699,10 +1701,13 @@ get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid)
        if (get_rel_relkind(indexrelid) != RELKIND_INDEX)
            continue;
 
-       /* Silently skip partitions which the user has no access to. */
-       if (!object_ownercheck(RelationRelationId, relid, GetUserId()) &&
-           (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) ||
-            IsSharedRelation(relid)))
+       /*
+        * It's possible that the user does not have privileges to CLUSTER the
+        * leaf partition despite having such privileges on the partitioned
+        * table.  We skip any partitions which the user is not permitted to
+        * CLUSTER.
+        */
+       if (!cluster_is_permitted_for_relation(relid, GetUserId()))
            continue;
 
        /* Use a permanent memory context for the result list */
@@ -1718,3 +1723,19 @@ get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid)
 
    return rtcs;
 }
+
+/*
+ * Return whether userid has privileges to CLUSTER relid.  If not, this
+ * function emits a WARNING.
+ */
+static bool
+cluster_is_permitted_for_relation(Oid relid, Oid userid)
+{
+   if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK)
+       return true;
+
+   ereport(WARNING,
+           (errmsg("permission denied to cluster \"%s\", skipping it",
+                   get_rel_name(relid))));
+   return false;
+}
index 943a48bfa7102c906ea3fca269ea4898ec0929f5..de89be8d759721aee500b3f076ea60ac2e4f9ca6 100644 (file)
@@ -28,6 +28,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
@@ -2947,6 +2948,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
    char        relkind;
    struct ReindexIndexCallbackState *state = arg;
    LOCKMODE    table_lockmode;
+   Oid         table_oid;
 
    /*
     * Lock level here should match table lock in reindex_index() for
@@ -2986,14 +2988,19 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
                 errmsg("\"%s\" is not an index", relation->relname)));
 
    /* Check permissions */
-   if (!object_ownercheck(RelationRelationId, relId, GetUserId()))
-       aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_INDEX, relation->relname);
+   table_oid = IndexGetRelation(relId, true);
+   if (OidIsValid(table_oid))
+   {
+       AclResult   aclresult;
+
+       aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
+       if (aclresult != ACLCHECK_OK)
+           aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
+   }
 
    /* Lock heap before index to avoid deadlock. */
    if (relId != oldRelId)
    {
-       Oid         table_oid = IndexGetRelation(relId, true);
-
        /*
         * If the OID isn't valid, it means the index was concurrently
         * dropped, which is not a problem for us; just return normally.
@@ -3029,7 +3036,7 @@ ReindexTable(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLev
                                       (params->options & REINDEXOPT_CONCURRENTLY) != 0 ?
                                       ShareUpdateExclusiveLock : ShareLock,
                                       0,
-                                      RangeVarCallbackOwnsTable, NULL);
+                                      RangeVarCallbackMaintainsTable, NULL);
 
    if (get_rel_relkind(heapOid) == RELKIND_PARTITIONED_TABLE)
        ReindexPartitions(stmt, heapOid, params, isTopLevel);
@@ -3113,7 +3120,8 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
    {
        objectOid = get_namespace_oid(objectName, false);
 
-       if (!object_ownercheck(NamespaceRelationId, objectOid, GetUserId()))
+       if (!object_ownercheck(NamespaceRelationId, objectOid, GetUserId()) &&
+           !has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
            aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SCHEMA,
                           objectName);
    }
@@ -3125,7 +3133,8 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("can only reindex the currently open database")));
-       if (!object_ownercheck(DatabaseRelationId, objectOid, GetUserId()))
+       if (!object_ownercheck(DatabaseRelationId, objectOid, GetUserId()) &&
+           !has_privs_of_role(GetUserId(), ROLE_PG_MAINTAIN))
            aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
                           get_database_name(objectOid));
    }
@@ -3197,15 +3206,12 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
            continue;
 
        /*
-        * The table can be reindexed if the user is superuser, the table
-        * owner, or the database/schema owner (but in the latter case, only
-        * if it's not a shared relation).  object_ownercheck includes the
-        * superuser case, and depending on objectKind we already know that
-        * the user has permission to run REINDEX on this database or schema
-        * per the permission checks at the beginning of this routine.
+        * We already checked privileges on the database or schema, but we
+        * further restrict reindexing shared catalogs to roles with the
+        * MAINTAIN privilege on the relation.
         */
        if (classtuple->relisshared &&
-           !object_ownercheck(RelationRelationId, relid, GetUserId()))
+           pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) != ACLCHECK_OK)
            continue;
 
        /*
index 09ae09cf5cce5a3844e39e79f92759d3a6a8f219..cd20ae76badbc2a8a63734a4adb9c836f0e901a1 100644 (file)
@@ -283,7 +283,7 @@ LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid)
    AclMode     aclmask;
 
    /* any of these privileges permit any lock mode */
-   aclmask = ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE;
+   aclmask = ACL_MAINTAIN | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE;
 
    /* SELECT privileges also permit ACCESS SHARE and below */
    if (lockmode <= AccessShareLock)
index 03373462f0ea4a82574dc57b1390253ca763cee0..6d09b755564e32bbe81bf1c3c3a59e29e4709a17 100644 (file)
@@ -160,7 +160,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
     */
    matviewOid = RangeVarGetRelidExtended(stmt->relation,
                                          lockmode, 0,
-                                         RangeVarCallbackOwnsTable, NULL);
+                                         RangeVarCallbackMaintainsTable,
+                                         NULL);
    matviewRel = table_open(matviewOid, NoLock);
    relowner = matviewRel->rd_rel->relowner;
 
index 7014be80396ad49ef8e523540adab0dcbee56eb3..2470265561a8d820ddad21980c3d95719f6cb7f5 100644 (file)
@@ -18096,15 +18096,16 @@ AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid,
  * This is intended as a callback for RangeVarGetRelidExtended().  It allows
  * the relation to be locked only if (1) it's a plain or partitioned table,
  * materialized view, or TOAST table and (2) the current user is the owner (or
- * the superuser).  This meets the permission-checking needs of CLUSTER,
- * REINDEX TABLE, and REFRESH MATERIALIZED VIEW; we expose it here so that it
- * can be used by all.
+ * the superuser) or has been granted MAINTAIN.  This meets the
+ * permission-checking needs of CLUSTER, REINDEX TABLE, and REFRESH
+ * MATERIALIZED VIEW; we expose it here so that it can be used by all.
  */
 void
-RangeVarCallbackOwnsTable(const RangeVar *relation,
-                         Oid relId, Oid oldRelId, void *arg)
+RangeVarCallbackMaintainsTable(const RangeVar *relation,
+                              Oid relId, Oid oldRelId, void *arg)
 {
    char        relkind;
+   AclResult   aclresult;
 
    /* Nothing to do if the relation was not found. */
    if (!OidIsValid(relId))
@@ -18125,8 +18126,11 @@ RangeVarCallbackOwnsTable(const RangeVar *relation,
                 errmsg("\"%s\" is not a table or materialized view", relation->relname)));
 
    /* Check permissions */
-   if (!object_ownercheck(RelationRelationId, relId, GetUserId()))
-       aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relId)), relation->relname);
+   aclresult = pg_class_aclcheck(relId, GetUserId(), ACL_MAINTAIN);
+   if (aclresult != ACLCHECK_OK)
+       aclcheck_error(aclresult,
+                      get_relkind_objtype(get_rel_relkind(relId)),
+                      relation->relname);
 }
 
 /*
index 223d33728d5b34c25539c76e3c47d1545a2c7639..e63c86cae45dc2699ddcfd269114656ca84f7351 100644 (file)
@@ -170,6 +170,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
    /* By default parallel vacuum is enabled */
    params.nworkers = 0;
 
+   /* Will be set later if we recurse to a TOAST table. */
+   params.toast_parent = InvalidOid;
+
    /*
     * Set this to an invalid value so it is clear whether or not a
     * BUFFER_USAGE_LIMIT was specified when making the access strategy.
@@ -695,32 +698,29 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy,
 }
 
 /*
- * Check if a given relation can be safely vacuumed or analyzed.  If the
- * user is not the relation owner, issue a WARNING log message and return
- * false to let the caller decide what to do with this relation.  This
- * routine is used to decide if a relation can be processed for VACUUM or
- * ANALYZE.
+ * Check if the current user has privileges to vacuum or analyze the relation.
+ * If not, issue a WARNING log message and return false to let the caller
+ * decide what to do with this relation.  This routine is used to decide if a
+ * relation can be processed for VACUUM or ANALYZE.
  */
 bool
-vacuum_is_relation_owner(Oid relid, Form_pg_class reltuple, bits32 options)
+vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
+                                bits32 options)
 {
    char       *relname;
 
    Assert((options & (VACOPT_VACUUM | VACOPT_ANALYZE)) != 0);
 
-   /*
-    * Check permissions.
-    *
-    * We allow the user to vacuum or analyze a table if he is superuser, the
-    * table owner, or the database owner (but in the latter case, only if
-    * it's not a shared relation).  object_ownercheck includes the superuser
-    * case.
-    *
-    * Note we choose to treat permissions failure as a WARNING and keep
-    * trying to vacuum or analyze the rest of the DB --- is this appropriate?
+   /*----------
+    * A role has privileges to vacuum or analyze the relation if any of the
+    * following are true:
+    *   - the role owns the current database and the relation is not shared
+    *   - the role has the MAINTAIN privilege on the relation
+    *----------
     */
-   if (object_ownercheck(RelationRelationId, relid, GetUserId()) ||
-       (object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) && !reltuple->relisshared))
+   if ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) &&
+        !reltuple->relisshared) ||
+       pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK)
        return true;
 
    relname = NameStr(reltuple->relname);
@@ -936,10 +936,10 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
        classForm = (Form_pg_class) GETSTRUCT(tuple);
 
        /*
-        * Make a returnable VacuumRelation for this rel if user is a proper
-        * owner.
+        * Make a returnable VacuumRelation for this rel if the user has the
+        * required privileges.
         */
-       if (vacuum_is_relation_owner(relid, classForm, options))
+       if (vacuum_is_permitted_for_relation(relid, classForm, options))
        {
            oldcontext = MemoryContextSwitchTo(vac_context);
            vacrels = lappend(vacrels, makeVacuumRelation(vrel->relation,
@@ -1036,7 +1036,7 @@ get_all_vacuum_rels(MemoryContext vac_context, int options)
            continue;
 
        /* check permissions of relation */
-       if (!vacuum_is_relation_owner(relid, classForm, options))
+       if (!vacuum_is_permitted_for_relation(relid, classForm, options))
            continue;
 
        /*
@@ -1952,6 +1952,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
    LOCKMODE    lmode;
    Relation    rel;
    LockRelId   lockrelid;
+   Oid         priv_relid;
    Oid         toast_relid;
    Oid         save_userid;
    int         save_sec_context;
@@ -2028,16 +2029,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
    }
 
    /*
-    * Check if relation needs to be skipped based on ownership.  This check
+    * When recursing to a TOAST table, check privileges on the parent.  NB:
+    * This is only safe to do because we hold a session lock on the main
+    * relation that prevents concurrent deletion.
+    */
+   if (OidIsValid(params->toast_parent))
+       priv_relid = params->toast_parent;
+   else
+       priv_relid = RelationGetRelid(rel);
+
+   /*
+    * Check if relation needs to be skipped based on privileges.  This check
     * happens also when building the relation list to vacuum for a manual
     * operation, and needs to be done additionally here as VACUUM could
-    * happen across multiple transactions where relation ownership could have
-    * changed in-between.  Make sure to only generate logs for VACUUM in this
-    * case.
+    * happen across multiple transactions where privileges could have changed
+    * in-between.  Make sure to only generate logs for VACUUM in this case.
     */
-   if (!vacuum_is_relation_owner(RelationGetRelid(rel),
-                                 rel->rd_rel,
-                                 params->options & VACOPT_VACUUM))
+   if (!vacuum_is_permitted_for_relation(priv_relid,
+                                         rel->rd_rel,
+                                         params->options & ~VACOPT_ANALYZE))
    {
        relation_close(rel, lmode);
        PopActiveSnapshot();
@@ -2224,9 +2234,15 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
    {
        VacuumParams toast_vacuum_params;
 
-       /* force VACOPT_PROCESS_MAIN so vacuum_rel() processes it */
+       /*
+        * Force VACOPT_PROCESS_MAIN so vacuum_rel() processes it.  Likewise,
+        * set toast_parent so that the privilege checks are done on the main
+        * relation.  NB: This is only safe to do because we hold a session
+        * lock on the main relation that prevents concurrent deletion.
+        */
        memcpy(&toast_vacuum_params, params, sizeof(VacuumParams));
        toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
+       toast_vacuum_params.toast_parent = relid;
 
        vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy);
    }
index 7d7fff6f5d56e016d5541929ff2763e6a375eb31..046ab46eccadf6bed165ea082fb2d99a40cb76a9 100644 (file)
@@ -2906,6 +2906,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
        tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age;
        tab->at_params.is_wraparound = wraparound;
        tab->at_params.log_min_duration = log_min_duration;
+       tab->at_params.toast_parent = InvalidOid;
        tab->at_storage_param_vac_cost_limit = avopts ?
            avopts->vacuum_cost_limit : 0;
        tab->at_storage_param_vac_cost_delay = avopts ?
index 83a11465b348e1d1370f0b25f7876ef84ac6d2ba..cf5d08576a47e2ea3a72bb4f0e37e12f5e67483d 100644 (file)
@@ -330,6 +330,9 @@ aclparse(const char *s, AclItem *aip, Node *escontext)
            case ACL_ALTER_SYSTEM_CHR:
                read = ACL_ALTER_SYSTEM;
                break;
+           case ACL_MAINTAIN_CHR:
+               read = ACL_MAINTAIN;
+               break;
            case 'R':           /* ignore old RULE privileges */
                read = 0;
                break;
@@ -1621,6 +1624,7 @@ makeaclitem(PG_FUNCTION_ARGS)
        {"CONNECT", ACL_CONNECT},
        {"SET", ACL_SET},
        {"ALTER SYSTEM", ACL_ALTER_SYSTEM},
+       {"MAINTAIN", ACL_MAINTAIN},
        {"RULE", 0},            /* ignore old RULE privileges */
        {NULL, 0}
    };
@@ -1729,6 +1733,8 @@ convert_aclright_to_string(int aclright)
            return "SET";
        case ACL_ALTER_SYSTEM:
            return "ALTER SYSTEM";
+       case ACL_MAINTAIN:
+           return "MAINTAIN";
        default:
            elog(ERROR, "unrecognized aclright: %d", aclright);
            return NULL;
@@ -2041,6 +2047,8 @@ convert_table_priv_string(text *priv_type_text)
        {"REFERENCES WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_REFERENCES)},
        {"TRIGGER", ACL_TRIGGER},
        {"TRIGGER WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_TRIGGER)},
+       {"MAINTAIN", ACL_MAINTAIN},
+       {"MAINTAIN WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_MAINTAIN)},
        {"RULE", 0},            /* ignore old RULE privileges */
        {"RULE WITH GRANT OPTION", 0},
        {NULL, 0}
index 78193c3c2b3aa57cd66aef450bfe7c8bfadabed3..5649859aa1e2d0402c041053e1f3d241533be0f1 100644 (file)
@@ -463,6 +463,7 @@ do { \
                CONVERT_PRIV('d', "DELETE");
                CONVERT_PRIV('t', "TRIGGER");
                CONVERT_PRIV('D', "TRUNCATE");
+               CONVERT_PRIV('m', "MAINTAIN");
            }
        }
 
index 00b5092713d06599f6b904839fe17ade9a7628e6..c8b489d94ef919ac49733febe26b14fb5e0c08ca 100644 (file)
@@ -794,7 +794,7 @@ my %tests = (
            \QREVOKE ALL ON TABLES FROM regress_dump_test_role;\E\n
            \QALTER DEFAULT PRIVILEGES \E
            \QFOR ROLE regress_dump_test_role \E
-           \QGRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLES TO regress_dump_test_role;\E
+           \QGRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLES TO regress_dump_test_role;\E
            /xm,
        like => { %full_runs, section_post_data => 1, },
        unlike => { no_privs => 1, },
index 73133ce7358f388fab5b2e5d6ab09661c699c5ac..19db069ee94c244c0800c3580ff5b196835a65db 100644 (file)
@@ -1152,7 +1152,7 @@ Keywords_for_list_of_owner_roles, "PUBLIC"
 #define Privilege_options_of_grant_and_revoke \
 "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", \
 "CREATE", "CONNECT", "TEMPORARY", "EXECUTE", "USAGE", "SET", "ALTER SYSTEM", \
-"ALL"
+"MAINTAIN", "ALL"
 
 /* ALTER PROCEDURE options */
 #define Alter_procedure_options \
@@ -3944,7 +3944,7 @@ psql_completion(const char *text, int start, int end)
        if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES"))
            COMPLETE_WITH("SELECT", "INSERT", "UPDATE",
                          "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER",
-                         "CREATE", "EXECUTE", "USAGE", "ALL");
+                         "CREATE", "EXECUTE", "USAGE", "MAINTAIN", "ALL");
        else if (TailMatches("GRANT"))
            COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_roles,
                                     Privilege_options_of_grant_and_revoke);
@@ -3996,7 +3996,7 @@ psql_completion(const char *text, int start, int end)
    else if (TailMatches("GRANT|REVOKE", MatchAny) ||
             TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny))
    {
-       if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|ALL"))
+       if (TailMatches("SELECT|INSERT|UPDATE|DELETE|TRUNCATE|REFERENCES|TRIGGER|CREATE|CONNECT|TEMPORARY|TEMP|EXECUTE|USAGE|MAINTAIN|ALL"))
            COMPLETE_WITH("ON");
        else if (TailMatches("GRANT", MatchAny))
            COMPLETE_WITH("TO");
index f085221155653b55d3eb1e658cd269953c4335fd..07793117162ca45dea35f6fe2d2564d3c1b2600b 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202403091
+#define CATALOG_VERSION_NO 202403131
 
 #endif
index 82a2ec2862ea05e21b594e76ad1476aae34a093c..55cabdda6f4d5b973faf0fa161db4986f24ae6bb 100644 (file)
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
   rolpassword => '_null_', rolvaliduntil => '_null_' },
+{ oid => '9256', oid_symbol => 'ROLE_PG_MAINTAIN',
+  rolname => 'pg_maintain', rolsuper => 'f', rolinherit => 't',
+  rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+  rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+  rolpassword => '_null_', rolvaliduntil => '_null_' },
 { oid => '4550', oid_symbol => 'ROLE_PG_USE_RESERVED_CONNECTIONS',
   rolname => 'pg_use_reserved_connections', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
index 60d9b8f5b5f6c845078d03ff620569de65f22c9c..85cbad3d0c20488373eeb16f9521ba4b2b282116 100644 (file)
@@ -98,8 +98,9 @@ extern void AtEOSubXact_on_commit_actions(bool isCommit,
                                          SubTransactionId mySubid,
                                          SubTransactionId parentSubid);
 
-extern void RangeVarCallbackOwnsTable(const RangeVar *relation,
-                                     Oid relId, Oid oldRelId, void *arg);
+extern void RangeVarCallbackMaintainsTable(const RangeVar *relation,
+                                          Oid relId, Oid oldRelId,
+                                          void *arg);
 
 extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
                                         Oid relId, Oid oldRelId, void *arg);
index 7f623b37fdc9b968b6cda144c29916c2214ce9e9..1182a967427025cc5287b380cf27c851b9dff7db 100644 (file)
@@ -228,6 +228,7 @@ typedef struct VacuumParams
                                     * default */
    VacOptValue index_cleanup;  /* Do index vacuum and cleanup */
    VacOptValue truncate;       /* Truncate empty pages at the end */
+   Oid         toast_parent;   /* for privilege checks when recursing */
 
    /*
     * The number of parallel vacuum workers.  0 by default which means choose
@@ -343,8 +344,8 @@ extern bool vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 extern bool vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs);
 extern void vac_update_datfrozenxid(void);
 extern void vacuum_delay_point(void);
-extern bool vacuum_is_relation_owner(Oid relid, Form_pg_class reltuple,
-                                    bits32 options);
+extern bool vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
+                                            bits32 options);
 extern Relation vacuum_open_relation(Oid relid, RangeVar *relation,
                                     bits32 options, bool verbose,
                                     LOCKMODE lmode);
index 2380821600a5b19cb9a4e9c5fbb9e75a17390155..aadaf67f57439d426c011cc8bfbf0a2c5760e95b 100644 (file)
@@ -87,7 +87,8 @@ typedef uint64 AclMode;           /* a bitmask of privilege bits */
 #define ACL_CONNECT        (1<<11) /* for databases */
 #define ACL_SET            (1<<12) /* for configuration parameters */
 #define ACL_ALTER_SYSTEM (1<<13)   /* for configuration parameters */
-#define N_ACL_RIGHTS   14      /* 1 plus the last 1<
+#define ACL_MAINTAIN   (1<<14) /* for relations */
+#define N_ACL_RIGHTS   15      /* 1 plus the last 1<
 #define ACL_NO_RIGHTS  0
 /* Currently, SELECT ... FOR [KEY] UPDATE/SHARE requires UPDATE privileges */
 #define ACL_SELECT_FOR_UPDATE  ACL_UPDATE
index a803eb12910dcf927c1dcce52a788bdb62e3a3bd..3a0baf30395b734f22173d931ad7607ce69a95b3 100644 (file)
@@ -148,15 +148,16 @@ typedef struct ArrayType Acl;
 #define ACL_CONNECT_CHR            'c'
 #define ACL_SET_CHR                's'
 #define ACL_ALTER_SYSTEM_CHR   'A'
+#define ACL_MAINTAIN_CHR       'm'
 
 /* string holding all privilege code chars, in order by bitmask position */
-#define ACL_ALL_RIGHTS_STR "arwdDxtXUCTcsA"
+#define ACL_ALL_RIGHTS_STR "arwdDxtXUCTcsAm"
 
 /*
  * Bitmasks defining "all rights" for each supported object type
  */
 #define ACL_ALL_RIGHTS_COLUMN      (ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
-#define ACL_ALL_RIGHTS_RELATION        (ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER)
+#define ACL_ALL_RIGHTS_RELATION        (ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE        (ACL_USAGE|ACL_SELECT|ACL_UPDATE)
 #define ACL_ALL_RIGHTS_DATABASE        (ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW         (ACL_USAGE)
index 7acb675c97df859b8aee53276e5a958abbac0350..7be9e56ef1388cea06ec84d53a9c4617b990aa91 100644 (file)
@@ -3,7 +3,7 @@ Parsed test spec with 2 sessions
 starting permutation: s1_begin s1_lock_parent s2_auth s2_cluster s1_commit s2_reset
 step s1_begin: BEGIN;
 step s1_lock_parent: LOCK cluster_part_tab IN SHARE UPDATE EXCLUSIVE MODE;
-step s2_auth: SET ROLE regress_cluster_part;
+step s2_auth: SET ROLE regress_cluster_part; SET client_min_messages = ERROR;
 step s2_cluster: CLUSTER cluster_part_tab USING cluster_part_ind; 
 step s1_commit: COMMIT;
 step s2_cluster: <... completed>
@@ -11,7 +11,7 @@ step s2_reset: RESET ROLE;
 
 starting permutation: s1_begin s2_auth s1_lock_parent s2_cluster s1_commit s2_reset
 step s1_begin: BEGIN;
-step s2_auth: SET ROLE regress_cluster_part;
+step s2_auth: SET ROLE regress_cluster_part; SET client_min_messages = ERROR;
 step s1_lock_parent: LOCK cluster_part_tab IN SHARE UPDATE EXCLUSIVE MODE;
 step s2_cluster: CLUSTER cluster_part_tab USING cluster_part_ind; 
 step s1_commit: COMMIT;
@@ -21,14 +21,14 @@ step s2_reset: RESET ROLE;
 starting permutation: s1_begin s1_lock_child s2_auth s2_cluster s1_commit s2_reset
 step s1_begin: BEGIN;
 step s1_lock_child: LOCK cluster_part_tab1 IN SHARE UPDATE EXCLUSIVE MODE;
-step s2_auth: SET ROLE regress_cluster_part;
+step s2_auth: SET ROLE regress_cluster_part; SET client_min_messages = ERROR;
 step s2_cluster: CLUSTER cluster_part_tab USING cluster_part_ind;
 step s1_commit: COMMIT;
 step s2_reset: RESET ROLE;
 
 starting permutation: s1_begin s2_auth s1_lock_child s2_cluster s1_commit s2_reset
 step s1_begin: BEGIN;
-step s2_auth: SET ROLE regress_cluster_part;
+step s2_auth: SET ROLE regress_cluster_part; SET client_min_messages = ERROR;
 step s1_lock_child: LOCK cluster_part_tab1 IN SHARE UPDATE EXCLUSIVE MODE;
 step s2_cluster: CLUSTER cluster_part_tab USING cluster_part_ind;
 step s1_commit: COMMIT;
index 5091f684a97e8807f52c98a20b3fb307a9020d23..4d38a7f49a1477f6ee8e65f2f963ab63fc3b4d8d 100644 (file)
@@ -23,7 +23,7 @@ step s1_lock_child     { LOCK cluster_part_tab1 IN SHARE UPDATE EXCLUSIVE MODE;
 step s1_commit         { COMMIT; }
 
 session s2
-step s2_auth           { SET ROLE regress_cluster_part; }
+step s2_auth           { SET ROLE regress_cluster_part; SET client_min_messages = ERROR; }
 step s2_cluster        { CLUSTER cluster_part_tab USING cluster_part_ind; }
 step s2_reset          { RESET ROLE; }
 
index 5be918229e50e95785e7c04aead78d0125971382..3cec72d9d4ff2bfd5ad6a9eb5866af2c4f11535c 100644 (file)
@@ -300,6 +300,17 @@ sub adjust_old_dumpfile
        $dump = _mash_view_qualifiers($dump);
    }
 
+   if ($old_version >= 14 && $old_version < 17)
+   {
+       # Fix up some privilege-set discrepancies.
+       $dump =~
+         s {^REVOKE SELECT,INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLE}
+           {REVOKE ALL ON TABLE}mg;
+       $dump =~
+         s {^(GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE),UPDATE ON TABLE}
+           {$1,MAINTAIN,UPDATE ON TABLE}mg;
+   }
+
    if ($old_version < 14)
    {
        # Remove mentions of extended hash functions.
index a666d89ef599ad0e9378136418b746216761de4a..4d40a6809ab46705673bc1e4a767e31443b90268 100644 (file)
@@ -353,7 +353,9 @@ INSERT INTO clstr_3 VALUES (1);
 -- this user can only cluster clstr_1 and clstr_3, but the latter
 -- has not been clustered
 SET SESSION AUTHORIZATION regress_clstr_user;
+SET client_min_messages = ERROR;  -- order of "skipping" warnings may vary
 CLUSTER;
+RESET client_min_messages;
 SELECT * FROM clstr_1 UNION ALL
   SELECT * FROM clstr_2 UNION ALL
   SELECT * FROM clstr_3;
@@ -501,12 +503,17 @@ CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1);
 CREATE ROLE regress_ptnowner;
 CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2);
 ALTER TABLE ptnowner1 OWNER TO regress_ptnowner;
+SET SESSION AUTHORIZATION regress_ptnowner;
+CLUSTER ptnowner USING ptnowner_i_idx;
+ERROR:  permission denied for table ptnowner
+RESET SESSION AUTHORIZATION;
 ALTER TABLE ptnowner OWNER TO regress_ptnowner;
 CREATE TEMP TABLE ptnowner_oldnodes AS
   SELECT oid, relname, relfilenode FROM pg_partition_tree('ptnowner') AS tree
   JOIN pg_class AS c ON c.oid=tree.relid;
 SET SESSION AUTHORIZATION regress_ptnowner;
 CLUSTER ptnowner USING ptnowner_i_idx;
+WARNING:  permission denied to cluster "ptnowner2", skipping it
 RESET SESSION AUTHORIZATION;
 SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a
   JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
index 79fa117cb54d3674aeda6749d8820d4dc66bb2ed..70ab47a92f23ac8b1338bd5028a1feaec19e83d5 100644 (file)
@@ -2832,9 +2832,9 @@ RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
 REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
+ERROR:  permission denied for table pg_toast_1260
 REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+ERROR:  permission denied for index pg_toast_1260_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
index 5a9ee5d2cd4f3adf1a398903ffb911e652ddefd5..74d9ff2998d7ae8988ce9ed2884459fd6a428530 100644 (file)
@@ -19,7 +19,7 @@ DETAIL:  privileges for table deptest
 REVOKE SELECT ON deptest FROM GROUP regress_dep_group;
 DROP GROUP regress_dep_group;
 -- can't drop the user if we revoke the privileges partially
-REVOKE SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON deptest FROM regress_dep_user;
+REVOKE SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, MAINTAIN ON deptest FROM regress_dep_user;
 DROP USER regress_dep_user;
 ERROR:  role "regress_dep_user" cannot be dropped because some objects depend on it
 DETAIL:  privileges for table deptest
@@ -67,21 +67,21 @@ CREATE TABLE deptest (a serial primary key, b text);
 GRANT ALL ON deptest1 TO regress_dep_user2;
 RESET SESSION AUTHORIZATION;
 \z deptest1
-                                               Access privileges
- Schema |   Name   | Type  |                 Access privileges                  | Column privileges | Policies 
---------+----------+-------+----------------------------------------------------+-------------------+----------
- public | deptest1 | table | regress_dep_user0=arwdDxt/regress_dep_user0       +|                   | 
-        |          |       | regress_dep_user1=a*r*w*d*D*x*t*/regress_dep_user0+|                   | 
-        |          |       | regress_dep_user2=arwdDxt/regress_dep_user1        |                   | 
+                                                Access privileges
+ Schema |   Name   | Type  |                  Access privileges                   | Column privileges | Policies 
+--------+----------+-------+------------------------------------------------------+-------------------+----------
+ public | deptest1 | table | regress_dep_user0=arwdDxtm/regress_dep_user0        +|                   | 
+        |          |       | regress_dep_user1=a*r*w*d*D*x*t*m*/regress_dep_user0+|                   | 
+        |          |       | regress_dep_user2=arwdDxtm/regress_dep_user1         |                   | 
 (1 row)
 
 DROP OWNED BY regress_dep_user1;
 -- all grants revoked
 \z deptest1
-                                           Access privileges
- Schema |   Name   | Type  |              Access privileges              | Column privileges | Policies 
---------+----------+-------+---------------------------------------------+-------------------+----------
- public | deptest1 | table | regress_dep_user0=arwdDxt/regress_dep_user0 |                   | 
+                                            Access privileges
+ Schema |   Name   | Type  |              Access privileges               | Column privileges | Policies 
+--------+----------+-------+----------------------------------------------+-------------------+----------
+ public | deptest1 | table | regress_dep_user0=arwdDxtm/regress_dep_user0 |                   | 
 (1 row)
 
 -- table was dropped
index 5ae5757bdeef57f16335707a1ebbb17bd8a7c8a4..eb4b762ea10f3df92c9f932334b67aa86cd29bc3 100644 (file)
@@ -2296,9 +2296,9 @@ SELECT pg_input_is_valid('regress_priv_user1=rY', 'aclitem');
 (1 row)
 
 SELECT * FROM pg_input_error_info('regress_priv_user1=rY', 'aclitem');
-                         message                         | detail | hint | sql_error_code 
----------------------------------------------------------+--------+------+----------------
- invalid mode character: must be one of "arwdDxtXUCTcsA" |        |      | 22P02
+                         message                          | detail | hint | sql_error_code 
+----------------------------------------------------------+--------+------+----------------
+ invalid mode character: must be one of "arwdDxtXUCTcsAm" |        |      | 22P02
 (1 row)
 
 --
@@ -2639,38 +2639,38 @@ set session role regress_priv_user4;
 grant select on dep_priv_test to regress_priv_user5;
 \dp dep_priv_test
                                                Access privileges
- Schema |     Name      | Type  |               Access privileges               | Column privileges | Policies 
---------+---------------+-------+-----------------------------------------------+-------------------+----------
- public | dep_priv_test | table | regress_priv_user1=arwdDxt/regress_priv_user1+|                   | 
-        |               |       | regress_priv_user2=r*/regress_priv_user1     +|                   | 
-        |               |       | regress_priv_user3=r*/regress_priv_user1     +|                   | 
-        |               |       | regress_priv_user4=r*/regress_priv_user2     +|                   | 
-        |               |       | regress_priv_user4=r*/regress_priv_user3     +|                   | 
-        |               |       | regress_priv_user5=r/regress_priv_user4       |                   | 
+ Schema |     Name      | Type  |               Access privileges                | Column privileges | Policies 
+--------+---------------+-------+------------------------------------------------+-------------------+----------
+ public | dep_priv_test | table | regress_priv_user1=arwdDxtm/regress_priv_user1+|                   | 
+        |               |       | regress_priv_user2=r*/regress_priv_user1      +|                   | 
+        |               |       | regress_priv_user3=r*/regress_priv_user1      +|                   | 
+        |               |       | regress_priv_user4=r*/regress_priv_user2      +|                   | 
+        |               |       | regress_priv_user4=r*/regress_priv_user3      +|                   | 
+        |               |       | regress_priv_user5=r/regress_priv_user4        |                   | 
 (1 row)
 
 set session role regress_priv_user2;
 revoke select on dep_priv_test from regress_priv_user4 cascade;
 \dp dep_priv_test
                                                Access privileges
- Schema |     Name      | Type  |               Access privileges               | Column privileges | Policies 
---------+---------------+-------+-----------------------------------------------+-------------------+----------
- public | dep_priv_test | table | regress_priv_user1=arwdDxt/regress_priv_user1+|                   | 
-        |               |       | regress_priv_user2=r*/regress_priv_user1     +|                   | 
-        |               |       | regress_priv_user3=r*/regress_priv_user1     +|                   | 
-        |               |       | regress_priv_user4=r*/regress_priv_user3     +|                   | 
-        |               |       | regress_priv_user5=r/regress_priv_user4       |                   | 
+ Schema |     Name      | Type  |               Access privileges                | Column privileges | Policies 
+--------+---------------+-------+------------------------------------------------+-------------------+----------
+ public | dep_priv_test | table | regress_priv_user1=arwdDxtm/regress_priv_user1+|                   | 
+        |               |       | regress_priv_user2=r*/regress_priv_user1      +|                   | 
+        |               |       | regress_priv_user3=r*/regress_priv_user1      +|                   | 
+        |               |       | regress_priv_user4=r*/regress_priv_user3      +|                   | 
+        |               |       | regress_priv_user5=r/regress_priv_user4        |                   | 
 (1 row)
 
 set session role regress_priv_user3;
 revoke select on dep_priv_test from regress_priv_user4 cascade;
 \dp dep_priv_test
                                                Access privileges
- Schema |     Name      | Type  |               Access privileges               | Column privileges | Policies 
---------+---------------+-------+-----------------------------------------------+-------------------+----------
- public | dep_priv_test | table | regress_priv_user1=arwdDxt/regress_priv_user1+|                   | 
-        |               |       | regress_priv_user2=r*/regress_priv_user1     +|                   | 
-        |               |       | regress_priv_user3=r*/regress_priv_user1      |                   | 
+ Schema |     Name      | Type  |               Access privileges                | Column privileges | Policies 
+--------+---------------+-------+------------------------------------------------+-------------------+----------
+ public | dep_priv_test | table | regress_priv_user1=arwdDxtm/regress_priv_user1+|                   | 
+        |               |       | regress_priv_user2=r*/regress_priv_user1      +|                   | 
+        |               |       | regress_priv_user3=r*/regress_priv_user1       |                   | 
 (1 row)
 
 set session role regress_priv_user1;
@@ -2800,6 +2800,20 @@ LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
 COMMIT;
 \c
 REVOKE TRUNCATE ON lock_table FROM regress_locktable_user;
+-- LOCK TABLE and MAINTAIN permission
+GRANT MAINTAIN ON lock_table TO regress_locktable_user;
+SET SESSION AUTHORIZATION regress_locktable_user;
+BEGIN;
+LOCK TABLE lock_table IN ACCESS SHARE MODE; -- should pass
+ROLLBACK;
+BEGIN;
+LOCK TABLE lock_table IN ROW EXCLUSIVE MODE; -- should pass
+COMMIT;
+BEGIN;
+LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
+COMMIT;
+\c
+REVOKE MAINTAIN ON lock_table FROM regress_locktable_user;
 -- clean up
 DROP TABLE lock_table;
 DROP USER regress_locktable_user;
@@ -2913,3 +2927,59 @@ DROP SCHEMA regress_roleoption;
 DROP ROLE regress_roleoption_protagonist;
 DROP ROLE regress_roleoption_donor;
 DROP ROLE regress_roleoption_recipient;
+-- MAINTAIN
+CREATE ROLE regress_no_maintain;
+CREATE ROLE regress_maintain;
+CREATE ROLE regress_maintain_all IN ROLE pg_maintain;
+CREATE TABLE maintain_test (a INT);
+CREATE INDEX ON maintain_test (a);
+GRANT MAINTAIN ON maintain_test TO regress_maintain;
+CREATE MATERIALIZED VIEW refresh_test AS SELECT 1;
+GRANT MAINTAIN ON refresh_test TO regress_maintain;
+CREATE SCHEMA reindex_test;
+-- negative tests; should fail
+SET ROLE regress_no_maintain;
+VACUUM maintain_test;
+WARNING:  permission denied to vacuum "maintain_test", skipping it
+ANALYZE maintain_test;
+WARNING:  permission denied to analyze "maintain_test", skipping it
+VACUUM (ANALYZE) maintain_test;
+WARNING:  permission denied to vacuum "maintain_test", skipping it
+CLUSTER maintain_test USING maintain_test_a_idx;
+ERROR:  permission denied for table maintain_test
+REFRESH MATERIALIZED VIEW refresh_test;
+ERROR:  permission denied for materialized view refresh_test
+REINDEX TABLE maintain_test;
+ERROR:  permission denied for table maintain_test
+REINDEX INDEX maintain_test_a_idx;
+ERROR:  permission denied for index maintain_test_a_idx
+REINDEX SCHEMA reindex_test;
+ERROR:  must be owner of schema reindex_test
+RESET ROLE;
+SET ROLE regress_maintain;
+VACUUM maintain_test;
+ANALYZE maintain_test;
+VACUUM (ANALYZE) maintain_test;
+CLUSTER maintain_test USING maintain_test_a_idx;
+REFRESH MATERIALIZED VIEW refresh_test;
+REINDEX TABLE maintain_test;
+REINDEX INDEX maintain_test_a_idx;
+REINDEX SCHEMA reindex_test;
+ERROR:  must be owner of schema reindex_test
+RESET ROLE;
+SET ROLE regress_maintain_all;
+VACUUM maintain_test;
+ANALYZE maintain_test;
+VACUUM (ANALYZE) maintain_test;
+CLUSTER maintain_test USING maintain_test_a_idx;
+REFRESH MATERIALIZED VIEW refresh_test;
+REINDEX TABLE maintain_test;
+REINDEX INDEX maintain_test_a_idx;
+REINDEX SCHEMA reindex_test;
+RESET ROLE;
+DROP TABLE maintain_test;
+DROP MATERIALIZED VIEW refresh_test;
+DROP SCHEMA reindex_test;
+DROP ROLE regress_no_maintain;
+DROP ROLE regress_maintain;
+DROP ROLE regress_maintain_all;
index 6988128aa4c348df2c886c00f7144fb3a4ad5dfa..4538f0c37d5bf9fb56c1ea589c68f563f9b5159a 100644 (file)
@@ -93,23 +93,23 @@ CREATE POLICY p2r ON document AS RESTRICTIVE TO regress_rls_dave
 CREATE POLICY p1r ON document AS RESTRICTIVE TO regress_rls_dave
     USING (cid <> 44);
 \dp
-                                                                  Access privileges
-       Schema       |   Name   | Type  |              Access privileges              | Column privileges |                  Policies                  
---------------------+----------+-------+---------------------------------------------+-------------------+--------------------------------------------
- regress_rls_schema | category | table | regress_rls_alice=arwdDxt/regress_rls_alice+|                   | 
-                    |          |       | =arwdDxt/regress_rls_alice                  |                   | 
- regress_rls_schema | document | table | regress_rls_alice=arwdDxt/regress_rls_alice+|                   | p1:                                       +
-                    |          |       | =arwdDxt/regress_rls_alice                  |                   |   (u): (dlevel <= ( SELECT uaccount.seclv +
-                    |          |       |                                             |                   |    FROM uaccount                          +
-                    |          |       |                                             |                   |   WHERE (uaccount.pguser = CURRENT_USER)))+
-                    |          |       |                                             |                   | p2r (RESTRICTIVE):                        +
-                    |          |       |                                             |                   |   (u): ((cid <> 44) AND (cid < 50))       +
-                    |          |       |                                             |                   |   to: regress_rls_dave                    +
-                    |          |       |                                             |                   | p1r (RESTRICTIVE):                        +
-                    |          |       |                                             |                   |   (u): (cid <> 44)                        +
-                    |          |       |                                             |                   |   to: regress_rls_dave
- regress_rls_schema | uaccount | table | regress_rls_alice=arwdDxt/regress_rls_alice+|                   | 
-                    |          |       | =r/regress_rls_alice                        |                   | 
+                                                                   Access privileges
+       Schema       |   Name   | Type  |              Access privileges               | Column privileges |                  Policies                  
+--------------------+----------+-------+----------------------------------------------+-------------------+--------------------------------------------
+ regress_rls_schema | category | table | regress_rls_alice=arwdDxtm/regress_rls_alice+|                   | 
+                    |          |       | =arwdDxtm/regress_rls_alice                  |                   | 
+ regress_rls_schema | document | table | regress_rls_alice=arwdDxtm/regress_rls_alice+|                   | p1:                                       +
+                    |          |       | =arwdDxtm/regress_rls_alice                  |                   |   (u): (dlevel <= ( SELECT uaccount.seclv +
+                    |          |       |                                              |                   |    FROM uaccount                          +
+                    |          |       |                                              |                   |   WHERE (uaccount.pguser = CURRENT_USER)))+
+                    |          |       |                                              |                   | p2r (RESTRICTIVE):                        +
+                    |          |       |                                              |                   |   (u): ((cid <> 44) AND (cid < 50))       +
+                    |          |       |                                              |                   |   to: regress_rls_dave                    +
+                    |          |       |                                              |                   | p1r (RESTRICTIVE):                        +
+                    |          |       |                                              |                   |   (u): (cid <> 44)                        +
+                    |          |       |                                              |                   |   to: regress_rls_dave
+ regress_rls_schema | uaccount | table | regress_rls_alice=arwdDxtm/regress_rls_alice+|                   | 
+                    |          |       | =r/regress_rls_alice                         |                   | 
 (3 rows)
 
 \d document
index 6cb9c926c06eb4e9f7eff611b3a73ac778066731..b7115f861044d7650892b8f5c1ba33a7d3c3084b 100644 (file)
@@ -145,7 +145,9 @@ INSERT INTO clstr_3 VALUES (1);
 -- this user can only cluster clstr_1 and clstr_3, but the latter
 -- has not been clustered
 SET SESSION AUTHORIZATION regress_clstr_user;
+SET client_min_messages = ERROR;  -- order of "skipping" warnings may vary
 CLUSTER;
+RESET client_min_messages;
 SELECT * FROM clstr_1 UNION ALL
   SELECT * FROM clstr_2 UNION ALL
   SELECT * FROM clstr_3;
@@ -236,6 +238,9 @@ CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1);
 CREATE ROLE regress_ptnowner;
 CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2);
 ALTER TABLE ptnowner1 OWNER TO regress_ptnowner;
+SET SESSION AUTHORIZATION regress_ptnowner;
+CLUSTER ptnowner USING ptnowner_i_idx;
+RESET SESSION AUTHORIZATION;
 ALTER TABLE ptnowner OWNER TO regress_ptnowner;
 CREATE TEMP TABLE ptnowner_oldnodes AS
   SELECT oid, relname, relfilenode FROM pg_partition_tree('ptnowner') AS tree
index 2559c62d0b89a0c80fe9b7bcd196404c04ce9f34..8d74ed7122c249494d677f3acbc8ca97892d6517 100644 (file)
@@ -21,7 +21,7 @@ REVOKE SELECT ON deptest FROM GROUP regress_dep_group;
 DROP GROUP regress_dep_group;
 
 -- can't drop the user if we revoke the privileges partially
-REVOKE SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON deptest FROM regress_dep_user;
+REVOKE SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, MAINTAIN ON deptest FROM regress_dep_user;
 DROP USER regress_dep_user;
 
 -- now we are OK to drop him
index 2ef15a9d8c5ff28218f65a117e7f99f756645d0d..eeb4c0029266068468a7706ca2dc4ab09798f33f 100644 (file)
@@ -1793,6 +1793,21 @@ COMMIT;
 \c
 REVOKE TRUNCATE ON lock_table FROM regress_locktable_user;
 
+-- LOCK TABLE and MAINTAIN permission
+GRANT MAINTAIN ON lock_table TO regress_locktable_user;
+SET SESSION AUTHORIZATION regress_locktable_user;
+BEGIN;
+LOCK TABLE lock_table IN ACCESS SHARE MODE; -- should pass
+ROLLBACK;
+BEGIN;
+LOCK TABLE lock_table IN ROW EXCLUSIVE MODE; -- should pass
+COMMIT;
+BEGIN;
+LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
+COMMIT;
+\c
+REVOKE MAINTAIN ON lock_table FROM regress_locktable_user;
+
 -- clean up
 DROP TABLE lock_table;
 DROP USER regress_locktable_user;
@@ -1876,3 +1891,55 @@ DROP SCHEMA regress_roleoption;
 DROP ROLE regress_roleoption_protagonist;
 DROP ROLE regress_roleoption_donor;
 DROP ROLE regress_roleoption_recipient;
+
+-- MAINTAIN
+CREATE ROLE regress_no_maintain;
+CREATE ROLE regress_maintain;
+CREATE ROLE regress_maintain_all IN ROLE pg_maintain;
+CREATE TABLE maintain_test (a INT);
+CREATE INDEX ON maintain_test (a);
+GRANT MAINTAIN ON maintain_test TO regress_maintain;
+CREATE MATERIALIZED VIEW refresh_test AS SELECT 1;
+GRANT MAINTAIN ON refresh_test TO regress_maintain;
+CREATE SCHEMA reindex_test;
+
+-- negative tests; should fail
+SET ROLE regress_no_maintain;
+VACUUM maintain_test;
+ANALYZE maintain_test;
+VACUUM (ANALYZE) maintain_test;
+CLUSTER maintain_test USING maintain_test_a_idx;
+REFRESH MATERIALIZED VIEW refresh_test;
+REINDEX TABLE maintain_test;
+REINDEX INDEX maintain_test_a_idx;
+REINDEX SCHEMA reindex_test;
+RESET ROLE;
+
+SET ROLE regress_maintain;
+VACUUM maintain_test;
+ANALYZE maintain_test;
+VACUUM (ANALYZE) maintain_test;
+CLUSTER maintain_test USING maintain_test_a_idx;
+REFRESH MATERIALIZED VIEW refresh_test;
+REINDEX TABLE maintain_test;
+REINDEX INDEX maintain_test_a_idx;
+REINDEX SCHEMA reindex_test;
+RESET ROLE;
+
+SET ROLE regress_maintain_all;
+VACUUM maintain_test;
+ANALYZE maintain_test;
+VACUUM (ANALYZE) maintain_test;
+CLUSTER maintain_test USING maintain_test_a_idx;
+REFRESH MATERIALIZED VIEW refresh_test;
+REINDEX TABLE maintain_test;
+REINDEX INDEX maintain_test_a_idx;
+REINDEX SCHEMA reindex_test;
+RESET ROLE;
+
+DROP TABLE maintain_test;
+DROP MATERIALIZED VIEW refresh_test;
+DROP SCHEMA reindex_test;
+DROP ROLE regress_no_maintain;
+DROP ROLE regress_maintain;
+DROP ROLE regress_maintain_all;