Fix has_column_privilege function corner case
authorJoe Conway <mail@joeconway.com>
Wed, 31 Mar 2021 17:55:25 +0000 (13:55 -0400)
committerJoe Conway <mail@joeconway.com>
Wed, 31 Mar 2021 17:55:25 +0000 (13:55 -0400)
According to the comments, when an invalid or dropped column oid is passed
to has_column_privilege(), the intention has always been to return NULL.
However, when the caller had table level privilege the invalid/missing
column was never discovered, because table permissions were checked first.

Fix that by introducing extended versions of pg_attribute_acl(check|mask)
and pg_class_acl(check|mask) which take a new argument, is_missing. When
is_missing is NULL, the old behavior is preserved. But when is_missing is
passed by the caller, no ERROR is thrown for dropped or missing
columns/relations, and is_missing is flipped to true. This in turn allows
has_column_privilege to check for column privileges first, providing the
desired semantics.

Not backpatched since it is a user visible behavioral change with no previous
complaints, and the fix is a bit on the invasive side.

Author: Joe Conway
Reviewed-By: Tom Lane
Reported by: Ian Barwick
Discussion: https://postgr.es/m/flat/9b5f4311-157b-4164-7fe7-077b4fe8ed84%40joeconway.com

src/backend/catalog/aclchk.c
src/backend/utils/adt/acl.c
src/include/utils/acl.h
src/test/regress/expected/privileges.out
src/test/regress/sql/privileges.sql

index add3d147e766cde6ee64af335cc1f6988f5c66f6..4b2afffb8faa4ca9475347774e5f46599078c5da 100644 (file)
@@ -3705,6 +3705,20 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid,
 AclMode
 pg_attribute_aclmask(Oid table_oid, AttrNumber attnum, Oid roleid,
                                         AclMode mask, AclMaskHow how)
+{
+       return pg_attribute_aclmask_ext(table_oid, attnum, roleid,
+                                                                       mask, how, NULL);
+}
+
+/*
+ * Exported routine for examining a user's privileges for a column
+ *
+ * Does the bulk of the work for pg_attribute_aclmask(), and allows other
+ * callers to avoid the missing attribute ERROR when is_missing is non-NULL.
+ */
+AclMode
+pg_attribute_aclmask_ext(Oid table_oid, AttrNumber attnum, Oid roleid,
+                                                AclMode mask, AclMaskHow how, bool *is_missing)
 {
        AclMode         result;
        HeapTuple       classTuple;
@@ -3723,18 +3737,38 @@ pg_attribute_aclmask(Oid table_oid, AttrNumber attnum, Oid roleid,
                                                           ObjectIdGetDatum(table_oid),
                                                           Int16GetDatum(attnum));
        if (!HeapTupleIsValid(attTuple))
-               ereport(ERROR,
-                               (errcode(ERRCODE_UNDEFINED_COLUMN),
-                                errmsg("attribute %d of relation with OID %u does not exist",
-                                               attnum, table_oid)));
+       {
+               if (is_missing != NULL)
+               {
+                       /* return "no privileges" instead of throwing an error */
+                       *is_missing = true;
+                       return 0;
+               }
+               else
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_UNDEFINED_COLUMN),
+                                        errmsg("attribute %d of relation with OID %u does not exist",
+                                                       attnum, table_oid)));
+       }
+
        attributeForm = (Form_pg_attribute) GETSTRUCT(attTuple);
 
-       /* Throw error on dropped columns, too */
+       /* Check dropped columns, too */
        if (attributeForm->attisdropped)
-               ereport(ERROR,
-                               (errcode(ERRCODE_UNDEFINED_COLUMN),
-                                errmsg("attribute %d of relation with OID %u does not exist",
-                                               attnum, table_oid)));
+       {
+               if (is_missing != NULL)
+               {
+                       /* return "no privileges" instead of throwing an error */
+                       *is_missing = true;
+                       ReleaseSysCache(attTuple);
+                       return 0;
+               }
+               else
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_UNDEFINED_COLUMN),
+                                        errmsg("attribute %d of relation with OID %u does not exist",
+                                                       attnum, table_oid)));
+       }
 
        aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
                                                           &isNull);
@@ -3790,6 +3824,19 @@ pg_attribute_aclmask(Oid table_oid, AttrNumber attnum, Oid roleid,
 AclMode
 pg_class_aclmask(Oid table_oid, Oid roleid,
                                 AclMode mask, AclMaskHow how)
+{
+       return pg_class_aclmask_ext(table_oid, roleid, mask, how, NULL);
+}
+
+/*
+ * Exported routine for examining a user's privileges for a table
+ *
+ * Does the bulk of the work for pg_class_aclmask(), and allows other
+ * callers to avoid the missing relation ERROR when is_missing is non-NULL.
+ */
+AclMode
+pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
+                                        AclMaskHow how, bool *is_missing)
 {
        AclMode         result;
        HeapTuple       tuple;
@@ -3804,10 +3851,20 @@ pg_class_aclmask(Oid table_oid, Oid roleid,
         */
        tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
        if (!HeapTupleIsValid(tuple))
-               ereport(ERROR,
-                               (errcode(ERRCODE_UNDEFINED_TABLE),
-                                errmsg("relation with OID %u does not exist",
-                                               table_oid)));
+       {
+               if (is_missing != NULL)
+               {
+                       /* return "no privileges" instead of throwing an error */
+                       *is_missing = true;
+                       return 0;
+               }
+               else
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_UNDEFINED_TABLE),
+                                        errmsg("relation with OID %u does not exist",
+                                                       table_oid)));
+       }
+
        classForm = (Form_pg_class) GETSTRUCT(tuple);
 
        /*
@@ -4468,7 +4525,22 @@ AclResult
 pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
                                          Oid roleid, AclMode mode)
 {
-       if (pg_attribute_aclmask(table_oid, attnum, roleid, mode, ACLMASK_ANY) != 0)
+       return pg_attribute_aclcheck_ext(table_oid, attnum, roleid, mode, NULL);
+}
+
+
+/*
+ * Exported routine for checking a user's access privileges to a column
+ *
+ * Does the bulk of the work for pg_attribute_aclcheck(), and allows other
+ * callers to avoid the missing attribute ERROR when is_missing is non-NULL.
+ */
+AclResult
+pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
+                                         Oid roleid, AclMode mode, bool *is_missing)
+{
+       if (pg_attribute_aclmask_ext(table_oid, attnum, roleid, mode,
+                                                                ACLMASK_ANY, is_missing) != 0)
                return ACLCHECK_OK;
        else
                return ACLCHECK_NO_PRIV;
@@ -4581,7 +4653,21 @@ pg_attribute_aclcheck_all(Oid table_oid, Oid roleid, AclMode mode,
 AclResult
 pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode)
 {
-       if (pg_class_aclmask(table_oid, roleid, mode, ACLMASK_ANY) != 0)
+       return pg_class_aclcheck_ext(table_oid, roleid, mode, NULL);
+}
+
+/*
+ * Exported routine for checking a user's access privileges to a table
+ *
+ * Does the bulk of the work for pg_class_aclcheck(), and allows other
+ * callers to avoid the missing relation ERROR when is_missing is non-NULL.
+ */
+AclResult
+pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
+                                         AclMode mode, bool *is_missing)
+{
+       if (pg_class_aclmask_ext(table_oid, roleid, mode,
+                                                        ACLMASK_ANY, is_missing) != 0)
                return ACLCHECK_OK;
        else
                return ACLCHECK_NO_PRIV;
index 9955c7c5c06cdd3de2a8be3104bee32ef93f5941..6a8c6a20eeae5a828ffd059342bc0707e5449e3c 100644 (file)
@@ -2444,8 +2444,7 @@ column_privilege_check(Oid tableoid, AttrNumber attnum,
                                           Oid roleid, AclMode mode)
 {
        AclResult       aclresult;
-       HeapTuple       attTuple;
-       Form_pg_attribute attributeForm;
+       bool            is_missing = false;
 
        /*
         * If convert_column_name failed, we can just return -1 immediately.
@@ -2454,42 +2453,25 @@ column_privilege_check(Oid tableoid, AttrNumber attnum,
                return -1;
 
        /*
-        * First check if we have the privilege at the table level.  We check
-        * existence of the pg_class row before risking calling pg_class_aclcheck.
-        * Note: it might seem there's a race condition against concurrent DROP,
-        * but really it's safe because there will be no syscache flush between
-        * here and there.  So if we see the row in the syscache, so will
-        * pg_class_aclcheck.
+        * Check for column-level privileges first. This serves in
+        * part as a check on whether the column even exists, so we
+        * need to do it before checking table-level privilege.
         */
-       if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(tableoid)))
+       aclresult = pg_attribute_aclcheck_ext(tableoid, attnum, roleid,
+                                                                                 mode, &is_missing);
+       if (aclresult == ACLCHECK_OK)
+               return 1;
+       else if (is_missing)
                return -1;
 
-       aclresult = pg_class_aclcheck(tableoid, roleid, mode);
-
+       /* Next check if we have the privilege at the table level */
+       aclresult = pg_class_aclcheck_ext(tableoid, roleid, mode, &is_missing);
        if (aclresult == ACLCHECK_OK)
-               return true;
-
-       /*
-        * No table privilege, so try per-column privileges.  Again, we have to
-        * check for dropped attribute first, and we rely on the syscache not to
-        * notice a concurrent drop before pg_attribute_aclcheck fetches the row.
-        */
-       attTuple = SearchSysCache2(ATTNUM,
-                                                          ObjectIdGetDatum(tableoid),
-                                                          Int16GetDatum(attnum));
-       if (!HeapTupleIsValid(attTuple))
-               return -1;
-       attributeForm = (Form_pg_attribute) GETSTRUCT(attTuple);
-       if (attributeForm->attisdropped)
-       {
-               ReleaseSysCache(attTuple);
+               return 1;
+       else if (is_missing)
                return -1;
-       }
-       ReleaseSysCache(attTuple);
-
-       aclresult = pg_attribute_aclcheck(tableoid, attnum, roleid, mode);
-
-       return (aclresult == ACLCHECK_OK);
+       else
+               return 0;
 }
 
 /*
index 541a438387bdf735adce76b0a83ffd8162e676e9..af771c901d1e1fc6e07c10afc290b95c3eefa669 100644 (file)
@@ -233,8 +233,14 @@ extern void RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid);
 
 extern AclMode pg_attribute_aclmask(Oid table_oid, AttrNumber attnum,
                                                                        Oid roleid, AclMode mask, AclMaskHow how);
+extern AclMode pg_attribute_aclmask_ext(Oid table_oid, AttrNumber attnum,
+                                                                               Oid roleid, AclMode mask,
+                                                                               AclMaskHow how, bool *is_missing);
 extern AclMode pg_class_aclmask(Oid table_oid, Oid roleid,
                                                                AclMode mask, AclMaskHow how);
+extern AclMode pg_class_aclmask_ext(Oid table_oid, Oid roleid,
+                                                                       AclMode mask, AclMaskHow how,
+                                                                       bool *is_missing);
 extern AclMode pg_database_aclmask(Oid db_oid, Oid roleid,
                                                                   AclMode mask, AclMaskHow how);
 extern AclMode pg_proc_aclmask(Oid proc_oid, Oid roleid,
@@ -256,9 +262,14 @@ extern AclMode pg_type_aclmask(Oid type_oid, Oid roleid,
 
 extern AclResult pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
                                                                           Oid roleid, AclMode mode);
+extern AclResult pg_attribute_aclcheck_ext(Oid table_oid, AttrNumber attnum,
+                                                                                  Oid roleid, AclMode mode,
+                                                                                  bool *is_missing);
 extern AclResult pg_attribute_aclcheck_all(Oid table_oid, Oid roleid,
                                                                                   AclMode mode, AclMaskHow how);
 extern AclResult pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode);
+extern AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
+                                                                          AclMode mode, bool *is_missing);
 extern AclResult pg_database_aclcheck(Oid db_oid, Oid roleid, AclMode mode);
 extern AclResult pg_proc_aclcheck(Oid proc_oid, Oid roleid, AclMode mode);
 extern AclResult pg_language_aclcheck(Oid lang_oid, Oid roleid, AclMode mode);
index 4903371991f7f19a0c50436f252bfcc875e9682c..89f3d5da46e9dbb038960c6afa35118feae8220d 100644 (file)
@@ -1362,7 +1362,13 @@ select has_column_privilege('mytable','........pg.dropped.2........','select');
 select has_column_privilege('mytable',2::int2,'select');
  has_column_privilege 
 ----------------------
- t
+(1 row)
+
+select has_column_privilege('mytable',99::int2,'select');
+ has_column_privilege 
+----------------------
 (1 row)
 
 revoke select on table mytable from regress_priv_user3;
@@ -1372,6 +1378,12 @@ select has_column_privilege('mytable',2::int2,'select');
  
 (1 row)
 
+select has_column_privilege('mytable',99::int2,'select');
+ has_column_privilege 
+----------------------
+(1 row)
+
 drop table mytable;
 -- Grant options
 SET SESSION AUTHORIZATION regress_priv_user1;
index 8dcd2199e0d4dbfc449363ce94af7b5ab2d898e5..22337310af31f8eeddf24c2fdc1153475a8815f5 100644 (file)
@@ -836,8 +836,10 @@ alter table mytable drop column f2;
 select has_column_privilege('mytable','f2','select');
 select has_column_privilege('mytable','........pg.dropped.2........','select');
 select has_column_privilege('mytable',2::int2,'select');
+select has_column_privilege('mytable',99::int2,'select');
 revoke select on table mytable from regress_priv_user3;
 select has_column_privilege('mytable',2::int2,'select');
+select has_column_privilege('mytable',99::int2,'select');
 drop table mytable;
 
 -- Grant options