From 6245502f9fc981d613c70016b45d6e2674d7d0cb Mon Sep 17 00:00:00 2001
From: Joel Jakobsson <github@compiler.org>
Date: Thu, 4 Jul 2024 22:56:37 +0200
Subject: [PATCH] Extend pg_get_acl to handle sub-object IDs.

This patch modifies the pg_get_acl function to accept a third objsubid param.
This enables the retrieval of ACLs for columns within a table.

Detailed changes:
- Updated func.sgml to document the new parameter and its usage.
- Modified pg_get_acl function in objectaddress.c to handle sub-object IDs.
- Adjusted the catalog entry in pg_proc.dat to include the new parameter.
- Enhanced regression tests in privileges.sql to test the new functionality.
---
 doc/src/sgml/func.sgml                   |  6 +--
 src/backend/catalog/objectaddress.c      | 67 +++++++++++++++++++-----
 src/include/catalog/pg_proc.dat          |  2 +-
 src/test/regress/expected/privileges.out | 56 ++++++++++++++++++--
 src/test/regress/sql/privileges.sql      | 16 ++++--
 5 files changed, 123 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 93ee3d4b60..aad944979f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -26592,12 +26592,12 @@ SELECT currval(pg_get_serial_sequence('sometable', 'id'));
         <indexterm>
          <primary>pg_get_acl</primary>
         </indexterm>
-        <function>pg_get_acl</function> ( <parameter>classid</parameter> <type>oid</type>, <parameter>objid</parameter> <type>oid</type> )
+        <function>pg_get_acl</function> ( <parameter>classid</parameter> <type>oid</type>, <parameter>objid</parameter> <type>oid</type>, <parameter>objsubid</parameter> <type>integer</type> )
         <returnvalue>aclitem[]</returnvalue>
        </para>
        <para>
         Returns the <acronym>ACL</acronym> for a database object, specified
-        by catalog OID and object OID. This function returns
+        by catalog OID, object OID and sub-object ID. This function returns
         <literal>NULL</literal> values for undefined objects.
        </para></entry>
       </row>
@@ -26723,7 +26723,7 @@ SELECT currval(pg_get_serial_sequence('sometable', 'id'));
 <programlisting>
 postgres=# SELECT
     (pg_identify_object(s.classid,s.objid,s.objsubid)).*,
-    pg_catalog.pg_get_acl(s.classid,s.objid) AS acl
+    pg_catalog.pg_get_acl(s.classid,s.objid,s.objsubid) AS acl
 FROM pg_catalog.pg_shdepend AS s
 JOIN pg_catalog.pg_database AS d
     ON d.datname = current_database() AND
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2983b9180f..659512b254 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -4364,13 +4364,14 @@ pg_identify_object_as_address(PG_FUNCTION_ARGS)
 
 /*
  * SQL-level callable function to obtain the ACL of a specified object, given
- * its catalog OID and object OID.
+ * its catalog OID, object OID and sub-object ID.
  */
 Datum
 pg_get_acl(PG_FUNCTION_ARGS)
 {
 	Oid			classId = PG_GETARG_OID(0);
 	Oid			objectId = PG_GETARG_OID(1);
+	int32		objsubid = PG_GETARG_INT32(2);
 	Oid			catalogId;
 	AttrNumber	Anum_acl;
 	Relation	rel;
@@ -4391,21 +4392,63 @@ pg_get_acl(PG_FUNCTION_ARGS)
 	if (Anum_acl == InvalidAttrNumber)
 		PG_RETURN_NULL();
 
-	rel = table_open(catalogId, AccessShareLock);
-
-	tup = get_catalog_object_by_oid(rel, get_object_attnum_oid(catalogId),
-									objectId);
-	if (!HeapTupleIsValid(tup))
+	/*
+	 * The procedure to retrive the ACL is different if the object is a table
+	 * and the sub-object is non-zero, which means we're dealing with a column
+	 * of a table. In this case, the objsubid is the attnum for the column.
+	 */
+	if (classId == RelationRelationId && objsubid != 0)
 	{
-		table_close(rel, AccessShareLock);
-		PG_RETURN_NULL();
+		AttrNumber	attnum = (AttrNumber) objsubid;
+
+		HeapTuple	attTuple;
+		bool		isNull;
+
+		attTuple = SearchSysCache2(ATTNUM,
+									ObjectIdGetDatum(objectId),
+									Int16GetDatum(attnum));
+
+		if (!HeapTupleIsValid(attTuple))
+			PG_RETURN_NULL();
+
+		/* ignore dropped columns */
+		if (((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+		{
+			ReleaseSysCache(attTuple);
+			PG_RETURN_NULL();
+		}
+
+		datum = SysCacheGetAttr(ATTNUM, attTuple,
+										Anum_pg_attribute_attacl,
+										&isNull);
+
+		/* no need to do anything for a NULL ACL */
+		if (isNull)
+		{
+			ReleaseSysCache(attTuple);
+			PG_RETURN_NULL();
+		}
+
+		ReleaseSysCache(attTuple);
 	}
+	else
+	{
+		rel = table_open(catalogId, AccessShareLock);
 
-	datum = heap_getattr(tup, Anum_acl, RelationGetDescr(rel), &isnull);
-	table_close(rel, AccessShareLock);
+		tup = get_catalog_object_by_oid(rel, get_object_attnum_oid(catalogId),
+										objectId);
+		if (!HeapTupleIsValid(tup))
+		{
+			table_close(rel, AccessShareLock);
+			PG_RETURN_NULL();
+		}
 
-	if (isnull)
-		PG_RETURN_NULL();
+		datum = heap_getattr(tup, Anum_acl, RelationGetDescr(rel), &isnull);
+		table_close(rel, AccessShareLock);
+
+		if (isnull)
+			PG_RETURN_NULL();
+	}
 
 	PG_RETURN_DATUM(datum);
 }
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e1001a4822..9654b3ba4e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6364,7 +6364,7 @@
 
 { oid => '6347', descr => 'get ACL for SQL object',
   proname => 'pg_get_acl', provolatile => 's', prorettype => '_aclitem',
-  proargtypes => 'oid oid', proargnames => '{classid,objid}',
+  proargtypes => 'oid oid int4', proargnames => '{classid,objid,objsubid}',
   prosrc => 'pg_get_acl' },
 
 { oid => '3839',
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 332bc584eb..c904cbef84 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -213,7 +213,7 @@ SELECT * FROM atest1;
 (0 rows)
 
 CREATE TABLE atest2 (col1 varchar(10), col2 boolean);
-SELECT pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid);
+SELECT pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid, 0);
  pg_get_acl 
 ------------
  
@@ -223,7 +223,7 @@ GRANT SELECT ON atest2 TO regress_priv_user2;
 GRANT UPDATE ON atest2 TO regress_priv_user3;
 GRANT INSERT ON atest2 TO regress_priv_user4 GRANTED BY CURRENT_USER;
 GRANT TRUNCATE ON atest2 TO regress_priv_user5 GRANTED BY CURRENT_ROLE;
-SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid));
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid, 0));
                      unnest                     
 ------------------------------------------------
  regress_priv_user1=arwdDxtm/regress_priv_user1
@@ -234,13 +234,13 @@ SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid));
 (5 rows)
 
 -- Invalid inputs
-SELECT pg_get_acl('pg_class'::regclass, 0); -- null
+SELECT pg_get_acl('pg_class'::regclass, 0, 0); -- null
  pg_get_acl 
 ------------
  
 (1 row)
 
-SELECT pg_get_acl(0, 0); -- null
+SELECT pg_get_acl(0, 0, 0); -- null
  pg_get_acl 
 ------------
  
@@ -651,8 +651,56 @@ ERROR:  permission denied for table atest2
 SET SESSION AUTHORIZATION regress_priv_user1;
 CREATE TABLE atest5 (one int, two int unique, three int, four int unique);
 CREATE TABLE atest6 (one int, two int, blue int);
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 1);
+ pg_get_acl 
+------------
+ 
+(1 row)
+
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 2);
+ pg_get_acl 
+------------
+ 
+(1 row)
+
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 3);
+ pg_get_acl 
+------------
+ 
+(1 row)
+
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 4);
+ pg_get_acl 
+------------
+ 
+(1 row)
+
 GRANT SELECT (one), INSERT (two), UPDATE (three) ON atest5 TO regress_priv_user4;
 GRANT ALL (one) ON atest5 TO regress_priv_user3;
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 1));
+                   unnest                   
+--------------------------------------------
+ regress_priv_user4=r/regress_priv_user1
+ regress_priv_user3=arwx/regress_priv_user1
+(2 rows)
+
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 2));
+                 unnest                  
+-----------------------------------------
+ regress_priv_user4=a/regress_priv_user1
+(1 row)
+
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 3));
+                 unnest                  
+-----------------------------------------
+ regress_priv_user4=w/regress_priv_user1
+(1 row)
+
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 4));
+ unnest 
+--------
+(0 rows)
+
 INSERT INTO atest5 VALUES (1,2,3);
 SET SESSION AUTHORIZATION regress_priv_user4;
 SELECT * FROM atest5; -- fail
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 980d19bde5..ea5c93cc57 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -183,16 +183,16 @@ GRANT SELECT ON atest1 TO regress_priv_user3, regress_priv_user4;
 SELECT * FROM atest1;
 
 CREATE TABLE atest2 (col1 varchar(10), col2 boolean);
-SELECT pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid);
+SELECT pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid, 0);
 GRANT SELECT ON atest2 TO regress_priv_user2;
 GRANT UPDATE ON atest2 TO regress_priv_user3;
 GRANT INSERT ON atest2 TO regress_priv_user4 GRANTED BY CURRENT_USER;
 GRANT TRUNCATE ON atest2 TO regress_priv_user5 GRANTED BY CURRENT_ROLE;
-SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid));
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest2'::regclass::oid, 0));
 
 -- Invalid inputs
-SELECT pg_get_acl('pg_class'::regclass, 0); -- null
-SELECT pg_get_acl(0, 0); -- null
+SELECT pg_get_acl('pg_class'::regclass, 0, 0); -- null
+SELECT pg_get_acl(0, 0, 0); -- null
 
 GRANT TRUNCATE ON atest2 TO regress_priv_user4 GRANTED BY regress_priv_user5;  -- error
 
@@ -437,8 +437,16 @@ SELECT * FROM atestv2; -- fail (even though regress_priv_user2 can access underl
 SET SESSION AUTHORIZATION regress_priv_user1;
 CREATE TABLE atest5 (one int, two int unique, three int, four int unique);
 CREATE TABLE atest6 (one int, two int, blue int);
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 1);
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 2);
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 3);
+SELECT pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 4);
 GRANT SELECT (one), INSERT (two), UPDATE (three) ON atest5 TO regress_priv_user4;
 GRANT ALL (one) ON atest5 TO regress_priv_user3;
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 1));
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 2));
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 3));
+SELECT unnest(pg_get_acl('pg_class'::regclass, 'atest5'::regclass::oid, 4));
 
 INSERT INTO atest5 VALUES (1,2,3);
 
-- 
2.45.1

