From 92f9b9f75e73b14ebff1330baefdee0d4bec9427 Mon Sep 17 00:00:00 2001
From: Nik Samokhvalov <nik@postgres.ai>
Date: Thu, 10 Sep 2026 15:42:23 -0700
Subject: [PATCH 1/2] Honor column-level SELECT privileges in RI fast-path
 checks

The referenced table's owner can have SELECT on all referenced columns
without having table-level SELECT.  Accept these column privileges in the
fast path, as the SPI query does, instead of rejecting a valid FK check.

Check only the referenced columns, not other index or table attributes.
Keep the schema check and the table-level privilege fast path unchanged.

Extend the foreign_key ACL tests to cover single and composite keys,
partial and unrelated column grants, and revocation after a successful
check.  The composite case also covers differing attribute/index order
and an INCLUDE column without SELECT privilege.
---
 src/backend/utils/adt/ri_triggers.c       | 25 +++++++++++++-----
 src/test/regress/expected/foreign_key.out | 31 +++++++++++++++++++++++
 src/test/regress/sql/foreign_key.sql      | 30 ++++++++++++++++++++++
 3 files changed, 79 insertions(+), 7 deletions(-)

diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6958f99..8c8edc1 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -290,7 +290,8 @@ static bool ri_FastPathProbeOne(Relation pk_rel, Relation idx_rel,
 static bool ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap,
 						   bool *concurrently_updated);
 static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo);
-static void ri_CheckPermissions(Relation query_rel);
+static void ri_CheckPermissions(Relation query_rel,
+								const RI_ConstraintInfo *riinfo);
 static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys,
 									 int nkeys, TupleTableSlot *new_slot);
 static void build_index_scankeys(const RI_ConstraintInfo *riinfo,
@@ -2802,7 +2803,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo,
 						   saved_sec_context |
 						   SECURITY_LOCAL_USERID_CHANGE |
 						   SECURITY_NOFORCE_RLS);
-	ri_CheckPermissions(pk_rel);
+	ri_CheckPermissions(pk_rel, riinfo);
 
 	/*
 	 * Begin the scan under the switched user id, so that any access method
@@ -2989,10 +2990,10 @@ ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo)
 /*
  * ri_CheckPermissions
  *   Check that the current user has permissions to look into the schema of
- *   and SELECT from 'query_rel'
+ *   and SELECT the referenced columns of 'query_rel'
  */
 static void
-ri_CheckPermissions(Relation query_rel)
+ri_CheckPermissions(Relation query_rel, const RI_ConstraintInfo *riinfo)
 {
 	AclResult	aclresult;
 
@@ -3007,9 +3008,19 @@ ri_CheckPermissions(Relation query_rel)
 	/* SELECT on relation. */
 	aclresult = pg_class_aclcheck(RelationGetRelid(query_rel), GetUserId(),
 								  ACL_SELECT);
-	if (aclresult != ACLCHECK_OK)
-		aclcheck_error(aclresult, OBJECT_TABLE,
-					   RelationGetRelationName(query_rel));
+	if (aclresult == ACLCHECK_OK)
+		return;
+
+	/* Otherwise, require SELECT on each referenced column, as SPI does. */
+	for (int i = 0; i < riinfo->nkeys; i++)
+	{
+		aclresult = pg_attribute_aclcheck(RelationGetRelid(query_rel),
+										  riinfo->pk_attnums[i], GetUserId(),
+										  ACL_SELECT);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLE,
+						   RelationGetRelationName(query_rel));
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 9cea669..3386a17 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -413,6 +413,37 @@ REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user;
 -- Inserting into FKTABLE should fail
 INSERT INTO FKTABLE VALUES (2, 6);
 ERROR:  permission denied for table pktable
+-- Column-level SELECT on the referenced key should suffice.
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (2, 6);
+DROP TABLE FKTABLE;
+DROP TABLE PKTABLE;
+-- Check column-level privileges with differing key and attribute orders.
+CREATE TABLE PKTABLE (payload text, ptest1 int, ptest2 int,
+                     PRIMARY KEY (ptest2, ptest1) INCLUDE (payload));
+CREATE TABLE FKTABLE (ftest1 int, ftest2 int,
+                     FOREIGN KEY (ftest1, ftest2) REFERENCES PKTABLE (ptest1, ptest2));
+INSERT INTO PKTABLE VALUES ('test', 1, 2);
+ALTER TABLE PKTABLE OWNER TO regress_foreign_key_user;
+REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user;
+-- An unrelated column grant is not enough.
+GRANT SELECT (payload) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+ERROR:  permission denied for table pktable
+-- Nor is a grant on only one of the referenced columns.
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+ERROR:  permission denied for table pktable
+-- Both key columns suffice, without access to the included column.
+REVOKE SELECT (payload) ON PKTABLE FROM regress_foreign_key_user;
+GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+-- Recheck privileges even after a successful check.
+REVOKE SELECT (ptest1) ON PKTABLE FROM regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+ERROR:  permission denied for table pktable
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
 DROP TABLE FKTABLE;
 DROP TABLE PKTABLE;
 DROP USER regress_foreign_key_user;
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 07d8921..2f857ce 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -301,6 +301,36 @@ REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user;
 -- Inserting into FKTABLE should fail
 INSERT INTO FKTABLE VALUES (2, 6);
 
+-- Column-level SELECT on the referenced key should suffice.
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (2, 6);
+
+DROP TABLE FKTABLE;
+DROP TABLE PKTABLE;
+
+-- Check column-level privileges with differing key and attribute orders.
+CREATE TABLE PKTABLE (payload text, ptest1 int, ptest2 int,
+                     PRIMARY KEY (ptest2, ptest1) INCLUDE (payload));
+CREATE TABLE FKTABLE (ftest1 int, ftest2 int,
+                     FOREIGN KEY (ftest1, ftest2) REFERENCES PKTABLE (ptest1, ptest2));
+INSERT INTO PKTABLE VALUES ('test', 1, 2);
+ALTER TABLE PKTABLE OWNER TO regress_foreign_key_user;
+REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user;
+-- An unrelated column grant is not enough.
+GRANT SELECT (payload) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+-- Nor is a grant on only one of the referenced columns.
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+-- Both key columns suffice, without access to the included column.
+REVOKE SELECT (payload) ON PKTABLE FROM regress_foreign_key_user;
+GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+-- Recheck privileges even after a successful check.
+REVOKE SELECT (ptest1) ON PKTABLE FROM regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
+GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user;
+INSERT INTO FKTABLE VALUES (1, 2);
 DROP TABLE FKTABLE;
 DROP TABLE PKTABLE;
 
-- 
2.50.1 (Apple Git-155)

