From ecb0fd33720fab91df1207e85704f382f55e1eb7 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 13 Mar 2024 14:49:26 -0500 Subject: [PATCH] Reintroduce MAINTAIN privilege and pg_maintain predefined role. 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 2af07e2f74, 59825d1639, 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 --- doc/src/sgml/ddl.sgml | 35 ++++-- doc/src/sgml/func.sgml | 2 +- .../sgml/ref/alter_default_privileges.sgml | 4 +- doc/src/sgml/ref/analyze.sgml | 6 +- doc/src/sgml/ref/cluster.sgml | 10 +- doc/src/sgml/ref/grant.sgml | 3 +- doc/src/sgml/ref/lock.sgml | 4 +- .../sgml/ref/refresh_materialized_view.sgml | 5 +- doc/src/sgml/ref/reindex.sgml | 23 ++-- doc/src/sgml/ref/revoke.sgml | 2 +- doc/src/sgml/ref/vacuum.sgml | 6 +- doc/src/sgml/user-manag.sgml | 12 ++ src/backend/catalog/aclchk.c | 15 +++ src/backend/commands/analyze.c | 13 +- src/backend/commands/cluster.c | 43 +++++-- src/backend/commands/indexcmds.c | 34 ++--- src/backend/commands/lockcmds.c | 2 +- src/backend/commands/matview.c | 3 +- src/backend/commands/tablecmds.c | 18 +-- src/backend/commands/vacuum.c | 76 +++++++----- src/backend/postmaster/autovacuum.c | 1 + src/backend/utils/adt/acl.c | 8 ++ src/bin/pg_dump/dumputils.c | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 2 +- src/bin/psql/tab-complete.c | 6 +- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_authid.dat | 5 + src/include/commands/tablecmds.h | 5 +- src/include/commands/vacuum.h | 5 +- src/include/nodes/parsenodes.h | 3 +- src/include/utils/acl.h | 5 +- .../expected/cluster-conflict-partition.out | 8 +- .../specs/cluster-conflict-partition.spec | 2 +- .../perl/PostgreSQL/Test/AdjustUpgrade.pm | 11 ++ src/test/regress/expected/cluster.out | 7 ++ src/test/regress/expected/create_index.out | 4 +- src/test/regress/expected/dependency.out | 22 ++-- src/test/regress/expected/privileges.out | 116 ++++++++++++++---- src/test/regress/expected/rowsecurity.out | 34 ++--- src/test/regress/sql/cluster.sql | 5 + src/test/regress/sql/dependency.sql | 2 +- src/test/regress/sql/privileges.sql | 67 ++++++++++ 42 files changed, 457 insertions(+), 180 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 9d7e2c756b..8616a8e9cc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1868,8 +1868,8 @@ ALTER TABLE products RENAME TO items; INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CREATE, CONNECT, TEMPORARY, - EXECUTE, USAGE, SET - and ALTER SYSTEM. + EXECUTE, USAGE, SET, + 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 VACUUM, ANALYZE, + CLUSTER, REFRESH 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) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0bb7aeb40e..91f1e69359 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -24232,7 +24232,7 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); are SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, - and TRIGGER. + TRIGGER, and MAINTAIN. diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 78744470c8..1de4c5c1b4 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -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 } [, ...] diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml index bad121a1f1..2b94b378e9 100644 --- a/doc/src/sgml/ref/analyze.sgml +++ b/doc/src/sgml/ref/analyze.sgml @@ -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. diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml index 24340e7b9b..c5760244e6 100644 --- a/doc/src/sgml/ref/cluster.sgml +++ b/doc/src/sgml/ref/cluster.sgml @@ -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 diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 9d27b7fcde..65b1fe7711 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -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 . diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml index 6ce2518de7..070855da18 100644 --- a/doc/src/sgml/ref/lock.sgml +++ b/doc/src/sgml/ref/lock.sgml @@ -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, UPDATE, DELETE, or TRUNCATE privileges on the table, any lockmode is permitted. If the user has diff --git a/doc/src/sgml/ref/refresh_materialized_view.sgml b/doc/src/sgml/ref/refresh_materialized_view.sgml index e4e5145058..8ed43ade80 100644 --- a/doc/src/sgml/ref/refresh_materialized_view.sgml +++ b/doc/src/sgml/ref/refresh_materialized_view.sgml @@ -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 diff --git a/doc/src/sgml/ref/reindex.sgml b/doc/src/sgml/ref/reindex.sgml index b214861742..2942dccf1e 100644 --- a/doc/src/sgml/ref/reindex.sgml +++ b/doc/src/sgml/ref/reindex.sgml @@ -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 DATABASE, REINDEX 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 DATABASE, REINDEX SCHEMA, + and REINDEX SYSTEM will skip indexes on shared catalogs + unless the user has the MAINTAIN privilege on the + catalog. diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 2db66bbf37..8df492281a 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -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 [, ...] } diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index df0dd71a37..9857b35627 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -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. diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index e026a4e271..07a16247d7 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -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 diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 023938682d..7abf3c2a74 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -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; } diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index 29b6a20f1e..28880bd299 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -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; diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index cea7c8ea36..c04886c409 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -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; +} diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 943a48bfa7..de89be8d75 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -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; /* diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c index 09ae09cf5c..cd20ae76ba 100644 --- a/src/backend/commands/lockcmds.c +++ b/src/backend/commands/lockcmds.c @@ -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) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 03373462f0..6d09b75556 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -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; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 7014be8039..2470265561 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -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); } /* diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 223d33728d..e63c86cae4 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -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); } diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 7d7fff6f5d..046ab46ecc 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -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 ? diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 83a11465b3..cf5d08576a 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -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} diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 78193c3c2b..5649859aa1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -463,6 +463,7 @@ do { \ CONVERT_PRIV('d', "DELETE"); CONVERT_PRIV('t', "TRIGGER"); CONVERT_PRIV('D', "TRUNCATE"); + CONVERT_PRIV('m', "MAINTAIN"); } } diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 00b5092713..c8b489d94e 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -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, }, diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 73133ce735..19db069ee9 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -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"); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index f085221155..0779311716 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202403091 +#define CATALOG_VERSION_NO 202403131 #endif diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 82a2ec2862..55cabdda6f 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -84,6 +84,11 @@ 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', diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 60d9b8f5b5..85cbad3d0c 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -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); diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 7f623b37fd..1182a96742 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -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); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2380821600..aadaf67f57 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -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< 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; diff --git a/src/test/isolation/specs/cluster-conflict-partition.spec b/src/test/isolation/specs/cluster-conflict-partition.spec index 5091f684a9..4d38a7f49a 100644 --- a/src/test/isolation/specs/cluster-conflict-partition.spec +++ b/src/test/isolation/specs/cluster-conflict-partition.spec @@ -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; } diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm index 5be918229e..3cec72d9d4 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm @@ -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. diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index a666d89ef5..4d40a6809a 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -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"; diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 79fa117cb5..70ab47a92f 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -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; diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 5a9ee5d2cd..74d9ff2998 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -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 diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index 5ae5757bde..eb4b762ea1 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -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; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 6988128aa4..4538f0c37d 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -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 diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql index 6cb9c926c0..b7115f8610 100644 --- a/src/test/regress/sql/cluster.sql +++ b/src/test/regress/sql/cluster.sql @@ -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 diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 2559c62d0b..8d74ed7122 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -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 diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index 2ef15a9d8c..eeb4c00292 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -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;