"has_column_privilege()" issue with attnums and non-existent columns

Started by Ian Lawrence Barwickabout 5 years ago19 messages
#1Ian Lawrence Barwick
barwick@gmail.com
2 attachment(s)

Greetings

Consider a table set up as follows:

CREATE TABLE foo (id INT, val TEXT);
ALTER TABLE foo DROP COLUMN val;

When specifying the name of a dropped or non-existent column, the function
"has_column_privilege()" works as expected:

postgres=# SELECT has_column_privilege('foo', 'val', 'SELECT');
ERROR: column "val" of relation "foo" does not exist

postgres=# SELECT has_column_privilege('foo', 'bar', 'SELECT');
ERROR: column "bar" of relation "foo" does not exist

However when specifying a dropped or non-existent attnum, it returns
"TRUE", which is unexpected:

postgres=# SELECT has_column_privilege('foo', 2::int2, 'SELECT');
has_column_privilege
----------------------
t
(1 row)

postgres=# SELECT has_column_privilege('foo', 999::int2, 'SELECT');
has_column_privilege
----------------------
t
(1 row)

The comment on the relevant code section in "src/backend/utils/adt/acl.c"
(related to "column_privilege_check()") indicate that NULL is the intended
return value in these cases:

Likewise, the variants that take an integer attnum
return NULL (rather than throwing an error) if there is no such
pg_attribute entry. All variants return NULL if an attisdropped
column is selected.

The unexpected "TRUE" value is a result of "column_privilege_check()" returning
TRUE if the user has table-level privileges. This returns a valid result with
the function variants where the column name is specified, as the calling
function will have already performed a check of the column through its call to
"convert_column_name()". However when the attnum is specified, the status of
the column never gets checked.

Attached patch resolves this by having "column_privilege_check()" callers
indicate whether they have checked the column. If this is the case, and if the
user as table-level privileges, "column_privilege_check()" can return as before
with no further lookups needed.

If the user has table-level privileges but the column was not checked (i.e
attnum provided), the column lookup is performed.

The regression tests did contain checks for dropped/non-existent attnums,
however one group was acting on a table where the user had no table-level
privilege, giving a fall-through to the column-level check; the other group
contained a check which was just plain wrong.

This issue appears to have been present since "has_column_privilege()" was
introduced in PostreSQL 8.4 (commit 7449427a1e6a099bc7e76164cb99a01d5e87237b),
so falls into the "obscure, extreme corner-case" category, OTOH seems worth
backpatching to supported versions.

The second patch adds a bunch of missing static prototypes to "acl.c",
on general
principles.

I'll add this to the next commitfest.

Regards

Ian Barwick

--
EnterpriseDB: https://www.enterprisedb.com

Attachments:

has_column_privilege-attnum-fix.v1.patchtext/x-patch; charset=US-ASCII; name=has_column_privilege-attnum-fix.v1.patchDownload
commit 7e22f2d245888c41b66ae214c226b4a101f27a89
Author: Ian Barwick <ian@2ndquadrant.com>
Date:   Tue Jan 12 13:40:31 2021 +0900

    Fix has_column_privilege() with attnums and non-existent columns
    
    The existence of a column should be confirmed even if the user has
    the table-level privilege, otherwise the function will happily report
    the user has privilege on a dropped or non-existent column if an
    invalid attnum is provided.

diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index c7f029e218..be5649b7ac 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -2447,7 +2447,7 @@ has_any_column_privilege_id_id(PG_FUNCTION_ARGS)
  */
 static int
 column_privilege_check(Oid tableoid, AttrNumber attnum,
-					   Oid roleid, AclMode mode)
+					   Oid roleid, AclMode mode, bool column_checked)
 {
 	AclResult	aclresult;
 	HeapTuple	attTuple;
@@ -2472,7 +2472,11 @@ column_privilege_check(Oid tableoid, AttrNumber attnum,
 
 	aclresult = pg_class_aclcheck(tableoid, roleid, mode);
 
-	if (aclresult == ACLCHECK_OK)
+	/*
+	 * Table-level privilege is present and the column has been checked by the
+	 * caller.
+	 */
+	if (aclresult == ACLCHECK_OK && column_checked == true)
 		return true;
 
 	/*
@@ -2493,6 +2497,16 @@ column_privilege_check(Oid tableoid, AttrNumber attnum,
 	}
 	ReleaseSysCache(attTuple);
 
+	/*
+	 * If the table level privilege exists, and the existence of the column has
+	 * been confirmed, we can skip the per-column check.
+	 */
+	if (aclresult == ACLCHECK_OK)
+		return true;
+
+	/*
+	 * No table privilege, so try per-column privileges.
+	 */
 	aclresult = pg_attribute_aclcheck(tableoid, attnum, roleid, mode);
 
 	return (aclresult == ACLCHECK_OK);
@@ -2521,7 +2535,7 @@ has_column_privilege_name_name_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2548,7 +2562,8 @@ has_column_privilege_name_name_attnum(PG_FUNCTION_ARGS)
 	tableoid = convert_table_name(tablename);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
+
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2575,7 +2590,7 @@ has_column_privilege_name_id_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2600,7 +2615,7 @@ has_column_privilege_name_id_attnum(PG_FUNCTION_ARGS)
 	roleid = get_role_oid_or_public(NameStr(*username));
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2627,7 +2642,7 @@ has_column_privilege_id_name_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2652,7 +2667,7 @@ has_column_privilege_id_name_attnum(PG_FUNCTION_ARGS)
 	tableoid = convert_table_name(tablename);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2677,7 +2692,7 @@ has_column_privilege_id_id_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2700,7 +2715,7 @@ has_column_privilege_id_id_attnum(PG_FUNCTION_ARGS)
 
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2729,7 +2744,7 @@ has_column_privilege_name_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2756,7 +2771,8 @@ has_column_privilege_name_attnum(PG_FUNCTION_ARGS)
 	tableoid = convert_table_name(tablename);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
+
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2783,7 +2799,7 @@ has_column_privilege_id_name(PG_FUNCTION_ARGS)
 	colattnum = convert_column_name(tableoid, column);
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, true);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
@@ -2808,7 +2824,7 @@ has_column_privilege_id_attnum(PG_FUNCTION_ARGS)
 	roleid = GetUserId();
 	mode = convert_column_priv_string(priv_type_text);
 
-	privresult = column_privilege_check(tableoid, colattnum, roleid, mode);
+	privresult = column_privilege_check(tableoid, colattnum, roleid, mode, false);
 	if (privresult < 0)
 		PG_RETURN_NULL();
 	PG_RETURN_BOOL(privresult);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 7754c20db4..284fc9f081 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -1275,7 +1275,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;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 4911ad4add..7b231d78f0 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -777,6 +777,7 @@ 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');
 drop table mytable;
acl-missing-prototypes.v1.patchtext/x-patch; charset=US-ASCII; name=acl-missing-prototypes.v1.patchDownload
commit e0da3c8aebdcc9b3af27c1ae10df97ee73abf3a2
Author: Ian Barwick <ian@2ndquadrant.com>
Date:   Tue Jan 12 14:23:47 2021 +0900

    Add missing function declarations to acl.c

diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index be5649b7ac..0a7b8fc46b 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -89,10 +89,15 @@ static void check_circularity(const Acl *old_acl, const AclItem *mod_aip,
 							  Oid ownerId);
 static Acl *recursive_revoke(Acl *acl, Oid grantee, AclMode revoke_privs,
 							 Oid ownerId, DropBehavior behavior);
+static AclMode aclmask_direct(const Acl *acl, Oid roleid, Oid ownerId,
+							  AclMode mask, AclMaskHow how);
 
 static AclMode convert_priv_string(text *priv_type_text);
 static AclMode convert_any_priv_string(text *priv_type_text,
 									   const priv_map *privileges);
+static const char *convert_aclright_to_string(int aclright);
+static int column_privilege_check(Oid tableoid, AttrNumber attnum,
+								  Oid roleid, AclMode mode, bool column_checked);
 
 static Oid	convert_table_name(text *tablename);
 static AclMode convert_table_priv_string(text *priv_type_text);
@@ -119,6 +124,10 @@ static AclMode convert_role_priv_string(text *priv_type_text);
 static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode);
 
 static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue);
+static bool has_rolinherit(Oid roleid);
+static List *roles_has_privs_of(Oid roleid);
+static List *roles_is_member_of(Oid roleid);
+static int count_one_bits(AclMode mask);
 
 
 /*
#2Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Ian Lawrence Barwick (#1)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 2021-01-12 06:53, Ian Lawrence Barwick wrote:

postgres=# SELECT has_column_privilege('foo', 999::int2, 'SELECT');
has_column_privilege
----------------------
t
(1 row)

The comment on the relevant code section in "src/backend/utils/adt/acl.c"
(related to "column_privilege_check()") indicate that NULL is the intended
return value in these cases:

Likewise, the variants that take an integer attnum
return NULL (rather than throwing an error) if there is no such
pg_attribute entry. All variants return NULL if an attisdropped
column is selected.

The unexpected "TRUE" value is a result of "column_privilege_check()" returning
TRUE if the user has table-level privileges. This returns a valid result with
the function variants where the column name is specified, as the calling
function will have already performed a check of the column through its call to
"convert_column_name()". However when the attnum is specified, the status of
the column never gets checked.

I'm not convinced the current behavior is wrong. Is there some
practical use case that is affected by this behavior?

The second patch adds a bunch of missing static prototypes to "acl.c",
on general
principles.

Why is this necessary?

#3Ian Lawrence Barwick
barwick@gmail.com
In reply to: Peter Eisentraut (#2)
Re: "has_column_privilege()" issue with attnums and non-existent columns

2021年1月28日(木) 17:18 Peter Eisentraut <peter.eisentraut@enterprisedb.com>:

On 2021-01-12 06:53, Ian Lawrence Barwick wrote:

postgres=# SELECT has_column_privilege('foo', 999::int2, 'SELECT');
has_column_privilege
----------------------
t
(1 row)

The comment on the relevant code section in "src/backend/utils/adt/acl.c"
(related to "column_privilege_check()") indicate that NULL is the

intended

return value in these cases:

Likewise, the variants that take an integer attnum
return NULL (rather than throwing an error) if there is no such
pg_attribute entry. All variants return NULL if an attisdropped
column is selected.

The unexpected "TRUE" value is a result of "column_privilege_check()"

returning

TRUE if the user has table-level privileges. This returns a valid

result with

the function variants where the column name is specified, as the calling
function will have already performed a check of the column through its

call to

"convert_column_name()". However when the attnum is specified, the

status of

the column never gets checked.

I'm not convinced the current behavior is wrong. Is there some
practical use case that is affected by this behavior?

I was poking around at the function with a view to using it for something
and was
curious what it did with bad input.

Providing the column name of a dropped column:

Me: "Hey Postgres, do I have privileges on the dropped column 'bar' of
my table 'foo'?"
Pg: "That column doesn't even exist - here, have an error instead."
Me: "Hey Postgres, does some other less-privileged user have privileges
on the
dropped column 'bar' of my table 'foo'?
Pg: "That column doesn't even exist - here, have an error instead."

Providing the attnum of a dropped column:

Me: "Hey Postgres, here's the attnum of the dropped column 'bar', does
some
other less-privileged user have privileges on that column?"
Pg: "That column doesn't even exist - here, have a NULL".
Me: "Hey Postgres, here's the attnum of the dropped column 'bar' on
this table
I own, do I have privileges on that column?"
Pg: "Yup. And feel free to throw any other smallint at me, I'll pretend
that
represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems the
intent was
to return NULL in all cases where an invalid attnum was provided, but that
gets
short-circuited by the assumption table owner = has privilege on any column.

Not the most urgent or exciting of issues, but seems inconsistent to me.

The second patch adds a bunch of missing static prototypes to "acl.c",
on general
principles.

Why is this necessary?

Not exactly necessary, but seemed odd some functions had prototypes, others
not.
I have no idea what the policy is, if any, and not something I would lose
sleep over,
just thought I'd note it in passing.

Regards

Ian Barwick

--
EnterpriseDB: https://www.enterprisedb.com

#4Joe Conway
mail@joeconway.com
In reply to: Ian Lawrence Barwick (#3)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 1/29/21 12:13 AM, Ian Lawrence Barwick wrote:

2021年1月28日(木) 17:18 Peter Eisentraut:
I'm not convinced the current behavior is wrong.  Is there some
practical use case that is affected by this behavior?

 
I was poking around at the function with a view to using it for something and was
curious what it did with bad input.

Providing the column name of a dropped column:

    Me: "Hey Postgres, do I have privileges on the dropped column 'bar' of my
table 'foo'?"
    Pg: "That column doesn't even exist - here, have an error instead."
    Me: "Hey Postgres, does some other less-privileged user have privileges on the
         dropped column 'bar' of my table 'foo'?
    Pg: "That column doesn't even exist - here, have an error instead."

Providing the attnum of a dropped column:

    Me: "Hey Postgres, here's the attnum of the dropped column 'bar', does some
         other less-privileged user have privileges on that column?"
    Pg: "That column doesn't even exist - here, have a NULL".
    Me: "Hey Postgres, here's the attnum of the dropped column 'bar' on this table
         I own, do I have privileges on that column?"
    Pg: "Yup. And feel free to throw any other smallint at me, I'll pretend that
         represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems the intent was
to return NULL in all cases where an invalid attnum was provided, but that gets
short-circuited by the assumption table owner = has privilege on any column.

Nicely illustrated :-)

Not the most urgent or exciting of issues, but seems inconsistent to me.

+1

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#5David Steele
david@pgmasters.net
In reply to: Joe Conway (#4)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 1/29/21 4:56 AM, Joe Conway wrote:

On 1/29/21 12:13 AM, Ian Lawrence Barwick wrote:

2021年1月28日(木) 17:18 Peter Eisentraut:
I'm not convinced the current behavior is wrong.  Is there some
practical use case that is affected by this behavior?

I was poking around at the function with a view to using it for something and was
curious what it did with bad input.

Providing the column name of a dropped column:

    Me: "Hey Postgres, do I have privileges on the dropped column 'bar' of my
table 'foo'?"
    Pg: "That column doesn't even exist - here, have an error instead."
    Me: "Hey Postgres, does some other less-privileged user have privileges on the
         dropped column 'bar' of my table 'foo'?
    Pg: "That column doesn't even exist - here, have an error instead."

Providing the attnum of a dropped column:

    Me: "Hey Postgres, here's the attnum of the dropped column 'bar', does some
         other less-privileged user have privileges on that column?"
    Pg: "That column doesn't even exist - here, have a NULL".
    Me: "Hey Postgres, here's the attnum of the dropped column 'bar' on this table
         I own, do I have privileges on that column?"
    Pg: "Yup. And feel free to throw any other smallint at me, I'll pretend that
         represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems the intent was
to return NULL in all cases where an invalid attnum was provided, but that gets
short-circuited by the assumption table owner = has privilege on any column.

Nicely illustrated :-)

Not the most urgent or exciting of issues, but seems inconsistent to me.

+1

Peter, did Ian's explanation answer your concerns?

Joe (or Peter), any interest in reviewing/committing this patch?

Regards,
--
-David
david@pgmasters.net

#6Joe Conway
mail@joeconway.com
In reply to: David Steele (#5)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/3/21 8:50 AM, David Steele wrote:

On 1/29/21 4:56 AM, Joe Conway wrote:

On 1/29/21 12:13 AM, Ian Lawrence Barwick wrote:

2021年1月28日(木) 17:18 Peter Eisentraut:
I'm not convinced the current behavior is wrong.  Is there some
practical use case that is affected by this behavior?

I was poking around at the function with a view to using it for something and was
curious what it did with bad input.

Providing the column name of a dropped column:

    Me: "Hey Postgres, do I have privileges on the dropped column 'bar' of my
table 'foo'?"
    Pg: "That column doesn't even exist - here, have an error instead."
    Me: "Hey Postgres, does some other less-privileged user have privileges on the
         dropped column 'bar' of my table 'foo'?
    Pg: "That column doesn't even exist - here, have an error instead."

Providing the attnum of a dropped column:

    Me: "Hey Postgres, here's the attnum of the dropped column 'bar', does some
         other less-privileged user have privileges on that column?"
    Pg: "That column doesn't even exist - here, have a NULL".
    Me: "Hey Postgres, here's the attnum of the dropped column 'bar' on this table
         I own, do I have privileges on that column?"
    Pg: "Yup. And feel free to throw any other smallint at me, I'll pretend that
         represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems the intent was
to return NULL in all cases where an invalid attnum was provided, but that gets
short-circuited by the assumption table owner = has privilege on any column.

Nicely illustrated :-)

Not the most urgent or exciting of issues, but seems inconsistent to me.

+1

Peter, did Ian's explanation answer your concerns?

Joe (or Peter), any interest in reviewing/committing this patch?

Sure, I'll take a look

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#7Joe Conway
mail@joeconway.com
In reply to: Joe Conway (#6)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/3/21 9:43 AM, Joe Conway wrote:

On 3/3/21 8:50 AM, David Steele wrote:

On 1/29/21 4:56 AM, Joe Conway wrote:

On 1/29/21 12:13 AM, Ian Lawrence Barwick wrote:

2021年1月28日(木) 17:18 Peter Eisentraut:
I'm not convinced the current behavior is wrong.  Is there some
practical use case that is affected by this behavior?

I was poking around at the function with a view to using it for something and was
curious what it did with bad input.

Providing the column name of a dropped column:

    Me: "Hey Postgres, do I have privileges on the dropped column 'bar' of my
table 'foo'?"
    Pg: "That column doesn't even exist - here, have an error instead."
    Me: "Hey Postgres, does some other less-privileged user have privileges on the
         dropped column 'bar' of my table 'foo'?
    Pg: "That column doesn't even exist - here, have an error instead."

Providing the attnum of a dropped column:

    Me: "Hey Postgres, here's the attnum of the dropped column 'bar', does some
         other less-privileged user have privileges on that column?"
    Pg: "That column doesn't even exist - here, have a NULL".
    Me: "Hey Postgres, here's the attnum of the dropped column 'bar' on this table
         I own, do I have privileges on that column?"
    Pg: "Yup. And feel free to throw any other smallint at me, I'll pretend that
         represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems the intent was
to return NULL in all cases where an invalid attnum was provided, but that gets
short-circuited by the assumption table owner = has privilege on any column.

Nicely illustrated :-)

Not the most urgent or exciting of issues, but seems inconsistent to me.

+1

Peter, did Ian's explanation answer your concerns?

Joe (or Peter), any interest in reviewing/committing this patch?

Sure, I'll take a look

Based on Tom's commit here:

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d0f68dd3

it appears to me that the intent is to return NULL.

However I was not to happy with the way the submitted patch added an argument to
column_privilege_check(). It seems to me that it is better to just reorder the
checks in column_privilege_check() a bit, as in the attached.

I wasn't entirely sure it was ok to split up the check for dropped attribute and
pg_attribute_aclcheck like I did, but when I tested the race condition (by
pausing at pg_attribute_aclcheck and dropping the column in another session)
everything seemed to work fine.

Any comments?

Thanks,

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#8Zhihong Yu
zyu@yugabyte.com
In reply to: Joe Conway (#7)
Re: "has_column_privilege()" issue with attnums and non-existent columns

Joe:
I don't seem to find attachment.

Maybe attach again ?

Thanks

On Sun, Mar 7, 2021 at 11:12 AM Joe Conway <mail@joeconway.com> wrote:

Show quoted text

On 3/3/21 9:43 AM, Joe Conway wrote:

On 3/3/21 8:50 AM, David Steele wrote:

On 1/29/21 4:56 AM, Joe Conway wrote:

On 1/29/21 12:13 AM, Ian Lawrence Barwick wrote:

2021年1月28日(木) 17:18 Peter Eisentraut:
I'm not convinced the current behavior is wrong. Is there some
practical use case that is affected by this behavior?

I was poking around at the function with a view to using it for

something and was

curious what it did with bad input.

Providing the column name of a dropped column:

Me: "Hey Postgres, do I have privileges on the dropped column

'bar' of my

table 'foo'?"
Pg: "That column doesn't even exist - here, have an error

instead."

Me: "Hey Postgres, does some other less-privileged user have

privileges on the

dropped column 'bar' of my table 'foo'?
Pg: "That column doesn't even exist - here, have an error

instead."

Providing the attnum of a dropped column:

Me: "Hey Postgres, here's the attnum of the dropped column

'bar', does some

other less-privileged user have privileges on that column?"
Pg: "That column doesn't even exist - here, have a NULL".
Me: "Hey Postgres, here's the attnum of the dropped column 'bar'

on this table

I own, do I have privileges on that column?"
Pg: "Yup. And feel free to throw any other smallint at me, I'll

pretend that

represents a column too even if it never existed.".

Looking at the code, particularly the cited comment, it does seems

the intent was

to return NULL in all cases where an invalid attnum was provided, but

that gets

short-circuited by the assumption table owner = has privilege on any

column.

Nicely illustrated :-)

Not the most urgent or exciting of issues, but seems inconsistent to

me.

+1

Peter, did Ian's explanation answer your concerns?

Joe (or Peter), any interest in reviewing/committing this patch?

Sure, I'll take a look

Based on Tom's commit here:

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d0f68dd3

it appears to me that the intent is to return NULL.

However I was not to happy with the way the submitted patch added an
argument to
column_privilege_check(). It seems to me that it is better to just reorder
the
checks in column_privilege_check() a bit, as in the attached.

I wasn't entirely sure it was ok to split up the check for dropped
attribute and
pg_attribute_aclcheck like I did, but when I tested the race condition (by
pausing at pg_attribute_aclcheck and dropping the column in another
session)
everything seemed to work fine.

Any comments?

Thanks,

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#9Joe Conway
mail@joeconway.com
In reply to: Zhihong Yu (#8)
1 attachment(s)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/7/21 2:35 PM, Zhihong Yu wrote:

Joe:
I don't seem to find attachment.

Maybe attach again ?

Oops -- I did forget that, didn't I. This time patch is attached :-)

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

Attachments:

has_column_privilege-attnum-fix-jec.00.patchtext/x-patch; charset=UTF-8; name=has_column_privilege-attnum-fix-jec.00.patchDownload
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index c7f029e..152d1da 100644
*** a/src/backend/utils/adt/acl.c
--- b/src/backend/utils/adt/acl.c
*************** column_privilege_check(Oid tableoid, Att
*** 2460,2484 ****
  		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.
! 	 */
! 	if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(tableoid)))
! 		return -1;
! 
! 	aclresult = pg_class_aclcheck(tableoid, roleid, mode);
! 
! 	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),
--- 2460,2468 ----
  		return -1;
  
  	/*
! 	 * 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),
*************** column_privilege_check(Oid tableoid, Att
*** 2493,2498 ****
--- 2477,2499 ----
  	}
  	ReleaseSysCache(attTuple);
  
+ 	/*
+ 	 * Now 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.
+ 	 */
+ 	if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(tableoid)))
+ 		return -1;
+ 
+ 	aclresult = pg_class_aclcheck(tableoid, roleid, mode);
+ 
+ 	if (aclresult == ACLCHECK_OK)
+ 		return true;
+ 
+ 	/* no table privilege, so try per-column privilege */
  	aclresult = pg_attribute_aclcheck(tableoid, attnum, roleid, mode);
  
  	return (aclresult == ACLCHECK_OK);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 46a69fc..d60ea53 100644
*** a/src/test/regress/expected/privileges.out
--- b/src/test/regress/expected/privileges.out
*************** select has_column_privilege('mytable','.
*** 1362,1368 ****
  select has_column_privilege('mytable',2::int2,'select');
   has_column_privilege 
  ----------------------
!  t
  (1 row)
  
  revoke select on table mytable from regress_priv_user3;
--- 1362,1374 ----
  select has_column_privilege('mytable',2::int2,'select');
   has_column_privilege 
  ----------------------
!  
! (1 row)
! 
! select has_column_privilege('mytable',99::int2,'select');
!  has_column_privilege 
! ----------------------
!  
  (1 row)
  
  revoke select on table mytable from regress_priv_user3;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6277140..766eeae 100644
*** a/src/test/regress/sql/privileges.sql
--- b/src/test/regress/sql/privileges.sql
*************** alter table mytable drop column f2;
*** 836,841 ****
--- 836,842 ----
  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');
  drop table mytable;
#10Chengxi Sun
sunchengxi@highgo.com
In reply to: Joe Conway (#9)
Re: "has_column_privilege()" issue with attnums and non-existent columns

The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not tested

I tested the patch and it works well. And according to the comment,
checking existence of relation and pg_class_aclcheck() won't influenced by concurrent DROP.
So I think it's safe to just reorder the checking existence of column and pg_attribute_aclcheck().

Thanks

#11Joe Conway
mail@joeconway.com
In reply to: Chengxi Sun (#10)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/16/21 1:42 AM, Chengxi Sun wrote:

The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not tested

I tested the patch and it works well.

Was that my patch (i.e. the latest on this thread) or Ian's? It is not clear
since you did not CC me...

And according to the comment, checking existence of relation and
pg_class_aclcheck() won't influenced by concurrent DROP. So I think it's safe
to just reorder the checking existence of column and
pg_attribute_aclcheck().

"Code never lies, comments sometimes do"

That said, I did at least a basic test of that assumption and it seems to be true.

Ian, or anyone else, any comments/complaints on my changes? If not I will commit
and push that version sooner rather than later.

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#12Joe Conway
mail@joeconway.com
In reply to: Joe Conway (#11)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/16/21 2:45 PM, Joe Conway wrote:

Ian, or anyone else, any comments/complaints on my changes? If not I will commit
and push that version sooner rather than later.

Any thoughts on back-patching this?

On one hand, in my view it is clearly a bug. On the other hand, no one has
complained before and this does change user visible behavior.

I guess I am leaning toward not back-patching...

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#13Tom Lane
tgl@sss.pgh.pa.us
In reply to: Joe Conway (#12)
Re: "has_column_privilege()" issue with attnums and non-existent columns

Joe Conway <mail@joeconway.com> writes:

On 3/16/21 2:45 PM, Joe Conway wrote:

Ian, or anyone else, any comments/complaints on my changes? If not I will commit
and push that version sooner rather than later.

I took a quick look, and I'm afraid I don't believe this:

! * 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.

What the existing code is assuming is that if we do a successful
SearchSysCache check, then an immediately following pg_xxx_aclcheck call
that needs to examine that same row will also find that row in the cache,
because there will be no need for any actual database traffic to do that.

What you've done here is changed that pattern to be

SearchSysCache(row1)

SearchSysCache(row2)

aclcheck on row1

aclcheck on row2

But that does NOT offer any such guarantee, because the row2 cache lookup
could incur a database access, which might notice that row1 has been
deleted.

I think we may have to adjust the acl.c APIs, or maybe better provide new
entry points, so that we can have variants of pg_xxx_aclcheck that won't
throw a hard error upon not finding the row. We cheesily tried to avoid
adjusting those APIs to support the semantics we need here, and we now see
that it didn't really work.

regards, tom lane

#14Joe Conway
mail@joeconway.com
In reply to: Tom Lane (#13)
1 attachment(s)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/21/21 12:27 PM, Tom Lane wrote:

I think we may have to adjust the acl.c APIs, or maybe better provide new
entry points, so that we can have variants of pg_xxx_aclcheck that won't
throw a hard error upon not finding the row. We cheesily tried to avoid
adjusting those APIs to support the semantics we need here, and we now see
that it didn't really work.

Ok, I took a shot at that; see attached.

Questions:

1. I confined the changes to just pg_class_aclcheck/mask
and pg_attribute_aclcheck/mask -- did you intend
that we do this same change across the board? Or
perhaps do the rest of them once we open up pg15
development?

2. This seems more invasive than something we would want
to back patch -- agreed?

Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

Attachments:

has_column_privilege-attnum-fix-jec.01.patchtext/x-patch; charset=UTF-8; name=has_column_privilege-attnum-fix-jec.01.patchDownload
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index add3d14..b5a6b3a 100644
*** a/src/backend/catalog/aclchk.c
--- b/src/backend/catalog/aclchk.c
*************** AclMode
*** 3706,3711 ****
--- 3706,3719 ----
  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);
+ }
+ 
+ AclMode
+ pg_attribute_aclmask_ext(Oid table_oid, AttrNumber attnum, Oid roleid,
+ 						 AclMode mask, AclMaskHow how, bool *is_missing)
+ {
  	AclMode		result;
  	HeapTuple	classTuple;
  	HeapTuple	attTuple;
*************** pg_attribute_aclmask(Oid table_oid, Attr
*** 3723,3740 ****
  							   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)));
  	attributeForm = (Form_pg_attribute) GETSTRUCT(attTuple);
  
! 	/* Throw error on 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)));
  
  	aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
  							   &isNull);
--- 3731,3768 ----
  							   ObjectIdGetDatum(table_oid),
  							   Int16GetDatum(attnum));
  	if (!HeapTupleIsValid(attTuple))
! 	{
! 		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);
  
! 	/* Check dropped columns, too */
  	if (attributeForm->attisdropped)
! 	{
! 		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);
*************** AclMode
*** 3791,3796 ****
--- 3819,3831 ----
  pg_class_aclmask(Oid table_oid, Oid roleid,
  				 AclMode mask, AclMaskHow how)
  {
+ 	return pg_class_aclmask_ext(table_oid, roleid, mask, how, NULL);
+ }
+ 
+ AclMode
+ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
+ 					 AclMaskHow how, bool *is_missing)
+ {
  	AclMode		result;
  	HeapTuple	tuple;
  	Form_pg_class classForm;
*************** pg_class_aclmask(Oid table_oid, Oid role
*** 3804,3813 ****
  	 */
  	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)));
  	classForm = (Form_pg_class) GETSTRUCT(tuple);
  
  	/*
--- 3839,3858 ----
  	 */
  	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
  	if (!HeapTupleIsValid(tuple))
! 	{
! 		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);
  
  	/*
*************** AclResult
*** 4468,4474 ****
  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 ACLCHECK_OK;
  	else
  		return ACLCHECK_NO_PRIV;
--- 4513,4527 ----
  pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
  					  Oid roleid, AclMode mode)
  {
! 	return pg_attribute_aclcheck_ext(table_oid, attnum, roleid, mode, 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;
*************** pg_attribute_aclcheck_all(Oid table_oid,
*** 4581,4587 ****
  AclResult
  pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode)
  {
! 	if (pg_class_aclmask(table_oid, roleid, mode, ACLMASK_ANY) != 0)
  		return ACLCHECK_OK;
  	else
  		return ACLCHECK_NO_PRIV;
--- 4634,4648 ----
  AclResult
  pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode)
  {
! 	return pg_class_aclcheck_ext(table_oid, roleid, mode, 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;
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index c7f029e..501a7a1 100644
*** a/src/backend/utils/adt/acl.c
--- b/src/backend/utils/adt/acl.c
*************** column_privilege_check(Oid tableoid, Att
*** 2450,2457 ****
  					   Oid roleid, AclMode mode)
  {
  	AclResult	aclresult;
! 	HeapTuple	attTuple;
! 	Form_pg_attribute attributeForm;
  
  	/*
  	 * If convert_column_name failed, we can just return -1 immediately.
--- 2450,2456 ----
  					   Oid roleid, AclMode mode)
  {
  	AclResult	aclresult;
! 	bool		is_missing = false;
  
  	/*
  	 * If convert_column_name failed, we can just return -1 immediately.
*************** column_privilege_check(Oid tableoid, Att
*** 2459,2501 ****
  	if (attnum == InvalidAttrNumber)
  		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.
! 	 */
! 	if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(tableoid)))
  		return -1;
  
! 	aclresult = pg_class_aclcheck(tableoid, roleid, mode);
! 
  	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;
! 	}
! 	ReleaseSysCache(attTuple);
! 
! 	aclresult = pg_attribute_aclcheck(tableoid, attnum, roleid, mode);
! 
! 	return (aclresult == ACLCHECK_OK);
  }
  
  /*
--- 2458,2479 ----
  	if (attnum == InvalidAttrNumber)
  		return -1;
  
! 	/* First 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 1;
! 	else if (is_missing)
  		return -1;
  
! 	/* No table privilege, so try per-column privileges */
! 	aclresult = pg_attribute_aclcheck_ext(tableoid, attnum, roleid,
! 										  mode, &is_missing);
  	if (aclresult == ACLCHECK_OK)
! 		return 1;
! 	else if (is_missing)
  		return -1;
! 	else
! 		return 0;
  }
  
  /*
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 541a438..af771c9 100644
*** a/src/include/utils/acl.h
--- b/src/include/utils/acl.h
*************** extern void RemoveRoleFromObjectACL(Oid
*** 233,240 ****
--- 233,246 ----
  
  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,
*************** extern AclMode pg_type_aclmask(Oid type_
*** 256,264 ****
--- 262,275 ----
  
  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);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 46a69fc..87733eb 100644
*** a/src/test/regress/expected/privileges.out
--- b/src/test/regress/expected/privileges.out
*************** select has_column_privilege('mytable',2:
*** 1372,1377 ****
--- 1372,1383 ----
   
  (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;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6277140..cd80f58 100644
*** a/src/test/regress/sql/privileges.sql
--- b/src/test/regress/sql/privileges.sql
*************** select has_column_privilege('mytable','.
*** 838,843 ****
--- 838,844 ----
  select has_column_privilege('mytable',2::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
#15Tom Lane
tgl@sss.pgh.pa.us
In reply to: Joe Conway (#14)
Re: "has_column_privilege()" issue with attnums and non-existent columns

Joe Conway <mail@joeconway.com> writes:

On 3/21/21 12:27 PM, Tom Lane wrote:

I think we may have to adjust the acl.c APIs, or maybe better provide new
entry points, so that we can have variants of pg_xxx_aclcheck that won't
throw a hard error upon not finding the row. We cheesily tried to avoid
adjusting those APIs to support the semantics we need here, and we now see
that it didn't really work.

Ok, I took a shot at that; see attached.

Looks generally reasonable by eyeball. The lack of any documentation
comment for the new functions makes me itch, although the ones that
are there are hardly better.

1. I confined the changes to just pg_class_aclcheck/mask
and pg_attribute_aclcheck/mask -- did you intend
that we do this same change across the board? Or
perhaps do the rest of them once we open up pg15
development?

In principle, it might be nice to fix all of those functions in acl.c
to be implemented similarly --- you could get rid of the initial
SearchSysCacheExists calls in the ones that are trying not to throw
error for is-missing cases. In practice, as long as there's no
reachable bug for the other cases, there are probably better things
to spend time on.

2. This seems more invasive than something we would want
to back patch -- agreed?

You could make an argument either way, but given the limited number
of complaints about this, I'd lean to no back-patch.

regards, tom lane

#16Joe Conway
mail@joeconway.com
In reply to: Joe Conway (#14)
1 attachment(s)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/30/21 3:37 PM, Joe Conway wrote:

On 3/21/21 12:27 PM, Tom Lane wrote:

I think we may have to adjust the acl.c APIs, or maybe better provide new
entry points, so that we can have variants of pg_xxx_aclcheck that won't
throw a hard error upon not finding the row. We cheesily tried to avoid
adjusting those APIs to support the semantics we need here, and we now see
that it didn't really work.

Ok, I took a shot at that; see attached.

Heh, I missed the forest for the trees it seems.

That version undid the changes fixing what Ian was originally complaining about.

New version attached that preserves the fixed behavior.

Questions:
1. I confined the changes to just pg_class_aclcheck/mask
and pg_attribute_aclcheck/mask -- did you intend
that we do this same change across the board? Or
perhaps do the rest of them once we open up pg15
development?

2. This seems more invasive than something we would want
to back patch -- agreed?

The questions remain...

Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

Attachments:

has_column_privilege-attnum-fix-jec.02.patchtext/x-patch; charset=UTF-8; name=has_column_privilege-attnum-fix-jec.02.patchDownload
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index add3d14..b5a6b3a 100644
*** a/src/backend/catalog/aclchk.c
--- b/src/backend/catalog/aclchk.c
*************** AclMode
*** 3706,3711 ****
--- 3706,3719 ----
  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);
+ }
+ 
+ AclMode
+ pg_attribute_aclmask_ext(Oid table_oid, AttrNumber attnum, Oid roleid,
+ 						 AclMode mask, AclMaskHow how, bool *is_missing)
+ {
  	AclMode		result;
  	HeapTuple	classTuple;
  	HeapTuple	attTuple;
*************** pg_attribute_aclmask(Oid table_oid, Attr
*** 3723,3740 ****
  							   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)));
  	attributeForm = (Form_pg_attribute) GETSTRUCT(attTuple);
  
! 	/* Throw error on 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)));
  
  	aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
  							   &isNull);
--- 3731,3768 ----
  							   ObjectIdGetDatum(table_oid),
  							   Int16GetDatum(attnum));
  	if (!HeapTupleIsValid(attTuple))
! 	{
! 		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);
  
! 	/* Check dropped columns, too */
  	if (attributeForm->attisdropped)
! 	{
! 		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);
*************** AclMode
*** 3791,3796 ****
--- 3819,3831 ----
  pg_class_aclmask(Oid table_oid, Oid roleid,
  				 AclMode mask, AclMaskHow how)
  {
+ 	return pg_class_aclmask_ext(table_oid, roleid, mask, how, NULL);
+ }
+ 
+ AclMode
+ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask,
+ 					 AclMaskHow how, bool *is_missing)
+ {
  	AclMode		result;
  	HeapTuple	tuple;
  	Form_pg_class classForm;
*************** pg_class_aclmask(Oid table_oid, Oid role
*** 3804,3813 ****
  	 */
  	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)));
  	classForm = (Form_pg_class) GETSTRUCT(tuple);
  
  	/*
--- 3839,3858 ----
  	 */
  	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
  	if (!HeapTupleIsValid(tuple))
! 	{
! 		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);
  
  	/*
*************** AclResult
*** 4468,4474 ****
  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 ACLCHECK_OK;
  	else
  		return ACLCHECK_NO_PRIV;
--- 4513,4527 ----
  pg_attribute_aclcheck(Oid table_oid, AttrNumber attnum,
  					  Oid roleid, AclMode mode)
  {
! 	return pg_attribute_aclcheck_ext(table_oid, attnum, roleid, mode, 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;
*************** pg_attribute_aclcheck_all(Oid table_oid,
*** 4581,4587 ****
  AclResult
  pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode)
  {
! 	if (pg_class_aclmask(table_oid, roleid, mode, ACLMASK_ANY) != 0)
  		return ACLCHECK_OK;
  	else
  		return ACLCHECK_NO_PRIV;
--- 4634,4648 ----
  AclResult
  pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode)
  {
! 	return pg_class_aclcheck_ext(table_oid, roleid, mode, 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;
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index c7f029e..cca543e 100644
*** a/src/backend/utils/adt/acl.c
--- b/src/backend/utils/adt/acl.c
*************** column_privilege_check(Oid tableoid, Att
*** 2450,2457 ****
  					   Oid roleid, AclMode mode)
  {
  	AclResult	aclresult;
! 	HeapTuple	attTuple;
! 	Form_pg_attribute attributeForm;
  
  	/*
  	 * If convert_column_name failed, we can just return -1 immediately.
--- 2450,2456 ----
  					   Oid roleid, AclMode mode)
  {
  	AclResult	aclresult;
! 	bool		is_missing = false;
  
  	/*
  	 * If convert_column_name failed, we can just return -1 immediately.
*************** column_privilege_check(Oid tableoid, Att
*** 2459,2501 ****
  	if (attnum == InvalidAttrNumber)
  		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.
! 	 */
! 	if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(tableoid)))
  		return -1;
  
! 	aclresult = pg_class_aclcheck(tableoid, roleid, mode);
! 
  	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;
! 	}
! 	ReleaseSysCache(attTuple);
! 
! 	aclresult = pg_attribute_aclcheck(tableoid, attnum, roleid, mode);
! 
! 	return (aclresult == ACLCHECK_OK);
  }
  
  /*
--- 2458,2479 ----
  	if (attnum == InvalidAttrNumber)
  		return -1;
  
! 	/* First try per-column privileges */
! 	aclresult = pg_attribute_aclcheck_ext(tableoid, attnum, roleid,
! 										  mode, &is_missing);
! 	if (aclresult == ACLCHECK_OK)
! 		return 1;
! 	else if (is_missing)
  		return -1;
  
! 	/* 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 1;
! 	else if (is_missing)
  		return -1;
! 	else
! 		return 0;
  }
  
  /*
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 541a438..af771c9 100644
*** a/src/include/utils/acl.h
--- b/src/include/utils/acl.h
*************** extern void RemoveRoleFromObjectACL(Oid
*** 233,240 ****
--- 233,246 ----
  
  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,
*************** extern AclMode pg_type_aclmask(Oid type_
*** 256,264 ****
--- 262,275 ----
  
  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);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 46a69fc..9857f6c 100644
*** a/src/test/regress/expected/privileges.out
--- b/src/test/regress/expected/privileges.out
*************** select has_column_privilege('mytable','.
*** 1362,1368 ****
  select has_column_privilege('mytable',2::int2,'select');
   has_column_privilege 
  ----------------------
!  t
  (1 row)
  
  revoke select on table mytable from regress_priv_user3;
--- 1362,1374 ----
  select has_column_privilege('mytable',2::int2,'select');
   has_column_privilege 
  ----------------------
!  
! (1 row)
! 
! select has_column_privilege('mytable',99::int2,'select');
!  has_column_privilege 
! ----------------------
!  
  (1 row)
  
  revoke select on table mytable from regress_priv_user3;
*************** select has_column_privilege('mytable',2:
*** 1370,1375 ****
--- 1376,1387 ----
   has_column_privilege 
  ----------------------
   
+ (1 row)
+ 
+ select has_column_privilege('mytable',99::int2,'select');
+  has_column_privilege 
+ ----------------------
+  
  (1 row)
  
  drop table mytable;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6277140..93f37fa 100644
*** a/src/test/regress/sql/privileges.sql
--- b/src/test/regress/sql/privileges.sql
*************** alter table mytable drop column f2;
*** 836,843 ****
--- 836,845 ----
  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
#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: Joe Conway (#16)
Re: "has_column_privilege()" issue with attnums and non-existent columns

Joe Conway <mail@joeconway.com> writes:

Heh, I missed the forest for the trees it seems.
That version undid the changes fixing what Ian was originally complaining about.

Duh, right. It would be a good idea for there to be a code comment
explaining this, because it's *far* from obvious. Say like

* 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.

My gripe about providing API-spec comments for the new aclchk.c
entry points still stands. Other than that, I think it's good
to go.

regards, tom lane

#18Joe Conway
mail@joeconway.com
In reply to: Tom Lane (#17)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/30/21 6:22 PM, Tom Lane wrote:

Joe Conway <mail@joeconway.com> writes:

Heh, I missed the forest for the trees it seems.
That version undid the changes fixing what Ian was originally complaining about.

Duh, right. It would be a good idea for there to be a code comment
explaining this, because it's *far* from obvious. Say like

* 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.

Will do.

My gripe about providing API-spec comments for the new aclchk.c
entry points still stands. Other than that, I think it's good
to go.

Yeah, I was planning to put something akin to this in all four spots:
8<-------------------
/*
* 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)
...
8<-------------------

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#19Joe Conway
mail@joeconway.com
In reply to: Joe Conway (#18)
Re: "has_column_privilege()" issue with attnums and non-existent columns

On 3/30/21 8:17 PM, Joe Conway wrote:

On 3/30/21 6:22 PM, Tom Lane wrote:

Joe Conway <mail@joeconway.com> writes:

Heh, I missed the forest for the trees it seems.
That version undid the changes fixing what Ian was originally complaining about.

Duh, right. It would be a good idea for there to be a code comment
explaining this, because it's *far* from obvious. Say like

* 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.

Will do.

My gripe about providing API-spec comments for the new aclchk.c
entry points still stands. Other than that, I think it's good
to go.

Yeah, I was planning to put something akin to this in all four spots:
8<-------------------
/*
* 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)
...
8<-------------------

Pushed that way.

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development