From 14197072f607d53d79e73a41dba76f83445bc78a Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 11 Sep 2026 12:14:20 +0900 Subject: [PATCH v2 1/2] Fix RI fast-path permission checks The fast path required table-level SELECT on the referenced table, rejecting checks that the SPI path allows with column-level grants. It also omitted the UPDATE privilege required by FOR KEY SHARE. When table privileges do not suffice, use ExecCheckOneRelPerms() with the referenced key columns as selectedCols and an empty updatedCols. This accepts SELECT on all referenced columns and UPDATE on any column, matching the SPI query. Keep the table-privilege check as a shortcut that avoids constructing a column bitmap in the usual case. Extend the ACL regression tests to cover column grants, privilege revocation, and per-row validation of a composite foreign key. Co-authored-by: Nikolay Samokhvalov Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 47 +++++++++++++----- src/test/regress/expected/foreign_key.out | 55 ++++++++++++++++++++- src/test/regress/sql/foreign_key.sql | 59 ++++++++++++++++++++++- 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index ea94b84ffc9..c62f7dc2d06 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -384,7 +384,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(const RI_ConstraintInfo *riinfo, + Relation query_rel); static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, TupleTableSlot *new_slot); static void build_index_scankeys(const RI_ConstraintInfo *riinfo, @@ -2920,7 +2921,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, saved_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_NOFORCE_RLS); - ri_CheckPermissions(pk_rel); + ri_CheckPermissions(riinfo, pk_rel); /* * Begin the scan under the switched user id, so that any access method @@ -3082,7 +3083,7 @@ ri_FastPathBatchFlush(RI_FastPathEntry *fpentry, Relation fk_rel, * albeit checked once per flush rather than once per row, like in * ri_FastPathCheck(). */ - ri_CheckPermissions(pk_rel); + ri_CheckPermissions(riinfo, pk_rel); /* * Begin the scan under the switched user id, so that any access method @@ -3505,13 +3506,16 @@ 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' + * Check permissions for the SELECT ... FOR KEY SHARE used by the SPI + * path, as the referenced table's owner. */ static void -ri_CheckPermissions(Relation query_rel) +ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel) { AclResult aclresult; + AclMode requiredPerms = ACL_SELECT | ACL_SELECT_FOR_UPDATE; + RTEPermissionInfo *perminfo; + bool result; /* USAGE on schema. */ aclresult = object_aclcheck(NamespaceRelationId, @@ -3521,11 +3525,32 @@ ri_CheckPermissions(Relation query_rel) aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(RelationGetNamespace(query_rel))); - /* SELECT on relation. */ - aclresult = pg_class_aclcheck(RelationGetRelid(query_rel), GetUserId(), - ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_TABLE, + /* Avoid building the column bitmap when table privileges suffice. */ + if (pg_class_aclmask(RelationGetRelid(query_rel), GetUserId(), + requiredPerms, ACLMASK_ALL) == requiredPerms) + return; + + /* + * SELECT is needed only on the referenced key columns. FOR KEY SHARE + * also needs UPDATE privilege, which may be granted on any column. Use + * the executor's checks for both, leaving updatedCols empty as the SPI + * query does. + */ + perminfo = makeNode(RTEPermissionInfo); + perminfo->relid = RelationGetRelid(query_rel); + perminfo->requiredPerms = requiredPerms; + for (int i = 0; i < riinfo->nkeys; i++) + { + int attno = riinfo->pk_attnums[i] - FirstLowInvalidHeapAttributeNumber; + + perminfo->selectedCols = bms_add_member(perminfo->selectedCols, attno); + } + + result = ExecCheckOneRelPerms(perminfo); + bms_free(perminfo->selectedCols); + pfree(perminfo); + if (!result) + aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_TABLE, RelationGetRelationName(query_rel)); } diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 8d81240f1c6..c54a9895b55 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -402,17 +402,68 @@ CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); INSERT INTO PKTABLE VALUES (1, 'Test1'); INSERT INTO PKTABLE VALUES (2, 'Test2'); INSERT INTO PKTABLE VALUES (3, 'Test3'); --- Grant usage on PKTABLE to user regress_foreign_key_user +-- Grant SELECT on PKTABLE to user regress_foreign_key_user CREATE USER regress_foreign_key_user NOLOGIN; GRANT SELECT ON PKTABLE TO regress_foreign_key_user; ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; -- Inserting into FKTABLE should work INSERT INTO FKTABLE VALUES (3, 5); --- Revoke usage on PKTABLE from user regress_foreign_key_user +-- Revoke SELECT on PKTABLE from user regress_foreign_key_user 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 +-- SELECT on the referenced key column is enough, without SELECT on ptest2. +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); +-- SELECT on an unrelated column does not suffice. +REVOKE SELECT (ptest1) ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails +ERROR: permission denied for table pktable +REVOKE SELECT (ptest2) ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; +-- FOR KEY SHARE also requires UPDATE privilege. +REVOKE UPDATE ON PKTABLE FROM regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails +ERROR: permission denied for table pktable +-- UPDATE on any column suffices, even one that the check does not read. +GRANT UPDATE (ptest2) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); +-- Table-level SELECT can be combined with column-level UPDATE. +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); +REVOKE UPDATE (ptest2) ON PKTABLE FROM regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails +ERROR: permission denied for table pktable +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; +-- Check all referenced columns, including when index and FK order differ. +CREATE TABLE PKTABLE ( ptest0 text, ptest1 int, ptest2 int, + PRIMARY KEY (ptest2, ptest1) ); +CREATE TABLE FKTABLE ( ftest1 int, ftest2 int ); +INSERT INTO PKTABLE VALUES ('unused', 1, 2); +INSERT INTO FKTABLE VALUES (1, 2); +ALTER TABLE FKTABLE ADD CONSTRAINT fktable_fk + FOREIGN KEY (ftest1, ftest2) REFERENCES PKTABLE (ptest1, ptest2) NOT VALID; +ALTER TABLE PKTABLE OWNER TO regress_foreign_key_user; +ALTER TABLE FKTABLE OWNER TO regress_foreign_key_user; +REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; +-- Lack of SELECT on FKTABLE forces validation to check each row. +REVOKE SELECT ON FKTABLE FROM regress_foreign_key_user; +SET ROLE regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; -- fails +ERROR: permission denied for table pktable +GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user; +-- Per-row validation also requires UPDATE privilege. +REVOKE UPDATE ON PKTABLE FROM regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; -- fails +ERROR: permission denied for table pktable +-- UPDATE on the unrelated column is enough. +GRANT UPDATE (ptest0) ON PKTABLE TO regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; +RESET ROLE; 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 184d9efdc97..c013d1f8834 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -286,7 +286,7 @@ INSERT INTO PKTABLE VALUES (1, 'Test1'); INSERT INTO PKTABLE VALUES (2, 'Test2'); INSERT INTO PKTABLE VALUES (3, 'Test3'); --- Grant usage on PKTABLE to user regress_foreign_key_user +-- Grant SELECT on PKTABLE to user regress_foreign_key_user CREATE USER regress_foreign_key_user NOLOGIN; GRANT SELECT ON PKTABLE TO regress_foreign_key_user; @@ -295,12 +295,67 @@ ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; -- Inserting into FKTABLE should work INSERT INTO FKTABLE VALUES (3, 5); --- Revoke usage on PKTABLE from user regress_foreign_key_user +-- Revoke SELECT on PKTABLE from user regress_foreign_key_user REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; -- Inserting into FKTABLE should fail INSERT INTO FKTABLE VALUES (2, 6); +-- SELECT on the referenced key column is enough, without SELECT on ptest2. +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); + +-- SELECT on an unrelated column does not suffice. +REVOKE SELECT (ptest1) ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails +REVOKE SELECT (ptest2) ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; + +-- FOR KEY SHARE also requires UPDATE privilege. +REVOKE UPDATE ON PKTABLE FROM regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails + +-- UPDATE on any column suffices, even one that the check does not read. +GRANT UPDATE (ptest2) ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); + +-- Table-level SELECT can be combined with column-level UPDATE. +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); +REVOKE UPDATE (ptest2) ON PKTABLE FROM regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (2, 6); -- fails + +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; + +-- Check all referenced columns, including when index and FK order differ. +CREATE TABLE PKTABLE ( ptest0 text, ptest1 int, ptest2 int, + PRIMARY KEY (ptest2, ptest1) ); +CREATE TABLE FKTABLE ( ftest1 int, ftest2 int ); +INSERT INTO PKTABLE VALUES ('unused', 1, 2); +INSERT INTO FKTABLE VALUES (1, 2); +ALTER TABLE FKTABLE ADD CONSTRAINT fktable_fk + FOREIGN KEY (ftest1, ftest2) REFERENCES PKTABLE (ptest1, ptest2) NOT VALID; +ALTER TABLE PKTABLE OWNER TO regress_foreign_key_user; +ALTER TABLE FKTABLE OWNER TO regress_foreign_key_user; +REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; +GRANT SELECT (ptest1) ON PKTABLE TO regress_foreign_key_user; + +-- Lack of SELECT on FKTABLE forces validation to check each row. +REVOKE SELECT ON FKTABLE FROM regress_foreign_key_user; +SET ROLE regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; -- fails +GRANT SELECT (ptest2) ON PKTABLE TO regress_foreign_key_user; + +-- Per-row validation also requires UPDATE privilege. +REVOKE UPDATE ON PKTABLE FROM regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; -- fails +-- UPDATE on the unrelated column is enough. +GRANT UPDATE (ptest0) ON PKTABLE TO regress_foreign_key_user; +ALTER TABLE FKTABLE VALIDATE CONSTRAINT fktable_fk; +RESET ROLE; + DROP TABLE FKTABLE; DROP TABLE PKTABLE; -- 2.47.3