The existing permissions for LOCK TABLE are surprising/confusing. For
instance, if you have UPDATE privileges on a table, you can lock in any
mode *except* ACCESS SHARE.

  drop table x cascade;
  drop user u1;
  create user u1;
  create table x(i int);
  grant update on x to u1;

  set session authorization u1;
  begin;
  lock table x in access exclusive mode; -- succeeds
  commit;
  begin;
  lock table x in share mode; -- succeeds
  commit;
  begin;
  lock table x in access share mode; -- fails
  commit;

I can't think of any reason for this behavior, and I didn't find an
obvious answer in the last commits to touch that (2ad36c4e44,
fa2642438f).

Patch attached to simplify it. It uses the philosophy that, if you have
permissions to lock at a given mode, you should be able to lock at
strictly less-conflicting modes as well.


-- 
Jeff Davis
PostgreSQL Contributor Team - AWS


From 53e27cfbe74f6b943fcf7969fb25ddf765219ff5 Mon Sep 17 00:00:00 2001
From: Jeff Davis <j...@j-davis.com>
Date: Tue, 13 Dec 2022 17:41:55 -0800
Subject: [PATCH v1] Rework permissions for LOCK TABLE.

The prior behavior was confusing and hard to document. For instance,
if you had UPDATE privileges, you could lock a table in any lock mode
except ACCESS SHARE mode.

Now, if granted a privilege to lock at a given mode, one also has
privileges to lock at a less-conflicting mode. MAINTAIN, UPDATE,
DELETE, and TRUNCATE privileges allow any lock mode. INSERT privileges
allow ROW EXCLUSIVE (or below). SELECT privileges allow ACCESS SHARE.
---
 doc/src/sgml/ref/lock.sgml               | 25 ++++-----
 src/backend/commands/lockcmds.c          | 18 +++----
 src/test/regress/expected/privileges.out | 66 ++++++++++--------------
 src/test/regress/sql/privileges.sql      | 61 +++++++++++-----------
 4 files changed, 79 insertions(+), 91 deletions(-)

diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index d9c5bf9a1d..8524182211 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -165,18 +165,19 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
   <title>Notes</title>
 
    <para>
-    To lock a table, one must ordinarily have the <literal>MAINTAIN</literal>
-    privilege on the table or be the table's owner, a superuser, or a role
-    with privileges of the
-    <link linkend="predefined-roles-table"><literal>pg_maintain</literal></link>
-    role. <literal>LOCK TABLE ... IN ACCESS SHARE MODE</literal> is allowed
-    with <literal>SELECT</literal> privileges on the target
-    table.  <literal>LOCK TABLE ... IN ROW EXCLUSIVE MODE</literal> is allowed
-    with <literal>INSERT</literal>, <literal>UPDATE</literal>, <literal>DELETE</literal>,
-    or <literal>TRUNCATE</literal> privileges on the target table. All other
-    forms of <command>LOCK</command> are allowed with
-    table-level <literal>UPDATE</literal>, <literal>DELETE</literal>,
-    or <literal>TRUNCATE</literal> privileges.
+    To lock a table, the user must have the right privilege for the specified
+    <replaceable class="parameter">lockmode</replaceable>, or be the table's
+    owner, a superuser, or a role with privileges of the <link
+    linkend="predefined-roles-table"><literal>pg_maintain</literal></link>
+    role. If the user has <literal>MAINTAIN</literal>,
+    <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+    <literal>TRUNCATE</literal> privileges on the table, any <replaceable
+    class="parameter">lockmode</replaceable> is permitted. If the user has
+    <literal>INSERT</literal> privileges on the table, <literal>ROW EXCLUSIVE
+    MODE</literal> (or a less-conflicting mode as described in <xref
+    linkend="explicit-locking"/>) is permitted. If a user has
+    <literal>SELECT</literal> privileges on the table, <literal>ACCESS SHARE
+    MODE</literal> is permitted.
    </para>
 
    <para>
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index e294efc67c..fceefafbce 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -292,16 +292,16 @@ LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid)
 	AclResult	aclresult;
 	AclMode		aclmask;
 
-	/* Verify adequate privilege */
-	if (lockmode == AccessShareLock)
-		aclmask = ACL_SELECT;
-	else if (lockmode == RowExclusiveLock)
-		aclmask = ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE;
-	else
-		aclmask = ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE;
+	/* any of these privileges permit any lock mode */
+	aclmask = ACL_MAINTAIN | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE;
+
+	/* SELECT privileges also permit ACCESS SHARE and below */
+	if (lockmode <= AccessShareLock)
+		aclmask |= ACL_SELECT;
 
-	/* MAINTAIN privilege allows all lock modes */
-	aclmask |= ACL_MAINTAIN;
+	/* INSERT privileges also permit ROW EXCLUSIVE and below */
+	if (lockmode <= RowExclusiveLock)
+		aclmask |= ACL_INSERT;
 
 	aclresult = pg_class_aclcheck(reloid, userid, aclmask);
 
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 169b364b22..58d9112ee8 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -2663,13 +2663,13 @@ CREATE TABLE lock_table (a int);
 GRANT SELECT ON lock_table TO regress_locktable_user;
 SET SESSION AUTHORIZATION regress_locktable_user;
 BEGIN;
+LOCK TABLE lock_table IN ACCESS SHARE MODE; -- should pass
+COMMIT;
+BEGIN;
 LOCK TABLE lock_table IN ROW EXCLUSIVE MODE; -- should fail
 ERROR:  permission denied for table lock_table
 ROLLBACK;
 BEGIN;
-LOCK TABLE lock_table IN ACCESS SHARE MODE; -- should pass
-COMMIT;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should fail
 ERROR:  permission denied for table lock_table
 ROLLBACK;
@@ -2679,13 +2679,12 @@ REVOKE SELECT ON lock_table FROM regress_locktable_user;
 GRANT INSERT 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 SHARE MODE; -- should fail
-ERROR:  permission denied for table lock_table
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should fail
 ERROR:  permission denied for table lock_table
 ROLLBACK;
@@ -2695,13 +2694,12 @@ REVOKE INSERT ON lock_table FROM regress_locktable_user;
 GRANT UPDATE 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 SHARE MODE; -- should fail
-ERROR:  permission denied for table lock_table
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
 COMMIT;
 \c
@@ -2710,13 +2708,12 @@ REVOKE UPDATE ON lock_table FROM regress_locktable_user;
 GRANT DELETE 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 SHARE MODE; -- should fail
-ERROR:  permission denied for table lock_table
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
 COMMIT;
 \c
@@ -2725,17 +2722,30 @@ REVOKE DELETE ON lock_table FROM regress_locktable_user;
 GRANT TRUNCATE 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 SHARE MODE; -- should fail
-ERROR:  permission denied for table lock_table
+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 TRUNCATE ON lock_table FROM regress_locktable_user;
+REVOKE MAINTAIN ON lock_table FROM regress_locktable_user;
 -- clean up
 DROP TABLE lock_table;
 DROP USER regress_locktable_user;
@@ -2877,14 +2887,6 @@ REINDEX INDEX maintain_test_a_idx;
 ERROR:  must be owner of index maintain_test_a_idx
 REINDEX SCHEMA reindex_test;
 ERROR:  must be owner of schema reindex_test
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-ERROR:  permission denied for table maintain_test
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-ERROR:  permission denied for table maintain_test
-COMMIT;
 RESET ROLE;
 SET ROLE regress_maintain;
 VACUUM maintain_test;
@@ -2896,12 +2898,6 @@ REINDEX TABLE maintain_test;
 REINDEX INDEX maintain_test_a_idx;
 REINDEX SCHEMA reindex_test;
 ERROR:  must be owner of schema reindex_test
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-COMMIT;
 RESET ROLE;
 SET ROLE regress_maintain_all;
 VACUUM maintain_test;
@@ -2912,12 +2908,6 @@ REFRESH MATERIALIZED VIEW refresh_test;
 REINDEX TABLE maintain_test;
 REINDEX INDEX maintain_test_a_idx;
 REINDEX SCHEMA reindex_test;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-COMMIT;
 RESET ROLE;
 DROP TABLE maintain_test;
 DROP MATERIALIZED VIEW refresh_test;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index b2db1c6dd5..f8efbd3061 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -1698,12 +1698,12 @@ CREATE TABLE lock_table (a int);
 GRANT SELECT ON lock_table TO regress_locktable_user;
 SET SESSION AUTHORIZATION regress_locktable_user;
 BEGIN;
-LOCK TABLE lock_table IN ROW EXCLUSIVE MODE; -- should fail
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS SHARE MODE; -- should pass
 COMMIT;
 BEGIN;
+LOCK TABLE lock_table IN ROW EXCLUSIVE MODE; -- should fail
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should fail
 ROLLBACK;
 \c
@@ -1713,12 +1713,12 @@ REVOKE SELECT ON lock_table FROM regress_locktable_user;
 GRANT INSERT 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 SHARE MODE; -- should fail
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should fail
 ROLLBACK;
 \c
@@ -1728,12 +1728,12 @@ REVOKE INSERT ON lock_table FROM regress_locktable_user;
 GRANT UPDATE 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 SHARE MODE; -- should fail
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
 COMMIT;
 \c
@@ -1743,12 +1743,12 @@ REVOKE UPDATE ON lock_table FROM regress_locktable_user;
 GRANT DELETE 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 SHARE MODE; -- should fail
-ROLLBACK;
-BEGIN;
 LOCK TABLE lock_table IN ACCESS EXCLUSIVE MODE; -- should pass
 COMMIT;
 \c
@@ -1758,16 +1758,31 @@ REVOKE DELETE ON lock_table FROM regress_locktable_user;
 GRANT TRUNCATE 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 SHARE MODE; -- should fail
+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 TRUNCATE ON lock_table FROM regress_locktable_user;
+REVOKE MAINTAIN ON lock_table FROM regress_locktable_user;
 
 -- clean up
 DROP TABLE lock_table;
@@ -1875,12 +1890,6 @@ REFRESH MATERIALIZED VIEW refresh_test;
 REINDEX TABLE maintain_test;
 REINDEX INDEX maintain_test_a_idx;
 REINDEX SCHEMA reindex_test;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-COMMIT;
 RESET ROLE;
 
 SET ROLE regress_maintain;
@@ -1892,12 +1901,6 @@ REFRESH MATERIALIZED VIEW refresh_test;
 REINDEX TABLE maintain_test;
 REINDEX INDEX maintain_test_a_idx;
 REINDEX SCHEMA reindex_test;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-COMMIT;
 RESET ROLE;
 
 SET ROLE regress_maintain_all;
@@ -1909,12 +1912,6 @@ REFRESH MATERIALIZED VIEW refresh_test;
 REINDEX TABLE maintain_test;
 REINDEX INDEX maintain_test_a_idx;
 REINDEX SCHEMA reindex_test;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS SHARE MODE;
-COMMIT;
-BEGIN;
-LOCK TABLE maintain_test IN ACCESS EXCLUSIVE MODE;
-COMMIT;
 RESET ROLE;
 
 DROP TABLE maintain_test;
-- 
2.34.1

Reply via email to