diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 40c1591ac7b..2797ac4abea 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -32,11 +32,13 @@ #include "access/tableam.h" #include "access/xact.h" #include "catalog/index.h" +#include "catalog/objectaccess.h" #include "catalog/pg_am_d.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_index.h" #include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/spi.h" @@ -399,6 +401,8 @@ 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 ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo, + const FastPathMeta *fpmeta); static void build_index_scankeys(const RI_ConstraintInfo *riinfo, FastPathMeta *fpmeta, Relation idx_rel, Datum *pk_vals, @@ -2971,6 +2975,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, ri_populate_fastpath_metadata(riinfo, fk_rel, idx_rel); } Assert(riinfo->fpmeta); + ri_CheckFunctionPermissions(riinfo, riinfo->fpmeta); ri_ExtractValues(fk_rel, newslot, riinfo, false, pk_vals, pk_nulls); build_index_scankeys(riinfo, riinfo->fpmeta, idx_rel, pk_vals, pk_nulls, skey); @@ -3715,6 +3720,41 @@ recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, return matched; } +/* + * ri_CheckFunctionPermissions + * Check EXECUTE privilege on the functions the fast path invokes on the + * FK values, as the referenced table's owner. + * + * This parallels the checks ExecInitFunc() performs when the SPI path + * initializes its generated query, where the equality operator's function + * appears in the WHERE clause and the cast function, if any, in the cast + * applied to the parameter. Call with the user id already switched to the + * referenced table's owner. + */ +static void +ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo, + const FastPathMeta *fpmeta) +{ + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid funcs[2] = {fpmeta->regops[i], fpmeta->cast_func_finfo[i].fn_oid}; + + for (int j = 0; j < lengthof(funcs); j++) + { + AclResult aclresult; + + if (!OidIsValid(funcs[j])) + continue; + aclresult = object_aclcheck(ProcedureRelationId, funcs[j], + GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + get_func_name(funcs[j])); + InvokeFunctionExecuteHook(funcs[j]); + } + } +} + /* * build_index_scankeys * Build ScanKeys for a direct index probe of the PK's unique index. diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 40be9c4f75d..f5b10c73c10 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -3803,6 +3803,87 @@ NOTICE: fp_auto_pk called NOTICE: fp_auto_pk called DROP TABLE fp_fk_cci, fp_pk_cci; DROP FUNCTION fp_auto_pk; +-- The fast path must check EXECUTE, as the referenced table's owner, on the +-- functions it invokes on the FK values: the equality operator's function +-- and, for a cross-type FK, the implicit cast function. Validation of a new +-- constraint by a role that cannot use the bulk check (RLS is enabled on the +-- referenced table) runs per-row checks through the fast path. +-- +-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere +-- with nearby tests. +BEGIN; +CREATE ROLE regress_ri_pkowner; +CREATE ROLE regress_ri_fkowner; +-- A private btree opclass whose equality function we can revoke without +-- affecting anything else. +CREATE FUNCTION regress_ri_int4eq(int4, int4) RETURNS bool + AS 'int4eq' LANGUAGE internal IMMUTABLE STRICT; +CREATE OPERATOR === (LEFTARG = int4, RIGHTARG = int4, + FUNCTION = regress_ri_int4eq, COMMUTATOR = ===, + RESTRICT = eqsel, JOIN = eqjoinsel); +CREATE OPERATOR CLASS regress_ri_int4_ops FOR TYPE int4 USING btree AS + OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >, + FUNCTION 1 btint4cmp(int4, int4); +REVOKE EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) TO regress_ri_fkowner; +-- An FK column type needing a user-defined implicit cast to the PK type. +CREATE TYPE fkenum AS ENUM ('one'); +CREATE FUNCTION fkenum_to_int4(fkenum) RETURNS int4 + AS 'SELECT 1' LANGUAGE sql IMMUTABLE STRICT; +CREATE CAST (fkenum AS int4) WITH FUNCTION fkenum_to_int4(fkenum) AS IMPLICIT; +REVOKE EXECUTE ON FUNCTION fkenum_to_int4(fkenum) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION fkenum_to_int4(fkenum) TO regress_ri_fkowner; +CREATE TABLE pktable_acl_op (a int4); +CREATE UNIQUE INDEX ON pktable_acl_op (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast (a int4 PRIMARY KEY); +CREATE TABLE pktable_acl_op_part (a int4) PARTITION BY LIST (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_op_part1 PARTITION OF pktable_acl_op_part DEFAULT; +CREATE UNIQUE INDEX ON pktable_acl_op_part (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast_part (a int4 PRIMARY KEY) PARTITION BY LIST (a); +INSERT INTO pktable_acl_op VALUES (1); +INSERT INTO pktable_acl_cast VALUES (1); +ALTER TABLE pktable_acl_op OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_op_part ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast_part ENABLE ROW LEVEL SECURITY; +GRANT REFERENCES ON pktable_acl_op, pktable_acl_cast, pktable_acl_op_part, pktable_acl_cast_part TO regress_ri_fkowner; +CREATE TABLE fktable_acl_op (a int4); +CREATE TABLE fktable_acl_cast (a fkenum); +INSERT INTO fktable_acl_op VALUES (1); +INSERT INTO fktable_acl_cast VALUES ('one'); +ALTER TABLE fktable_acl_op OWNER TO regress_ri_fkowner; +ALTER TABLE fktable_acl_cast OWNER TO regress_ri_fkowner; +-- The FK owner holds EXECUTE on both functions; the PK owner does not, and +-- it is the PK owner whose privileges apply. +SET ROLE regress_ri_fkowner; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op (a); -- fails +ERROR: permission denied for function regress_ri_int4eq +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails +ERROR: permission denied for function fkenum_to_int4 +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op_part (a); -- fails (SPI path message) +ERROR: permission denied for function regress_ri_int4eq +CONTEXT: SQL statement "SELECT 1 FROM "public"."pktable_acl_op_part" x WHERE "a" OPERATOR(public.===) $1 FOR KEY SHARE OF x" +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast_part (a); -- fails (SPI path message) +ERROR: permission denied for function fkenum_to_int4 +CONTEXT: SQL statement "SELECT 1 FROM "public"."pktable_acl_cast_part" x WHERE "a" OPERATOR(pg_catalog.=) $1::pg_catalog.int4 FOR KEY SHARE OF x" +ROLLBACK TO SAVEPOINT s; +RESET ROLE; +ROLLBACK; -- A STABLE cast used by an FK check must see changes made by earlier AFTER -- triggers, using the check's snapshot rather than the outer query's snapshot. -- Compare the per-row fast path with a partitioned-parent SPI check. diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 89405dff99e..bd5a47a836c 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2768,6 +2768,87 @@ INSERT INTO fp_fk_cci VALUES (1), (2), (3); DROP TABLE fp_fk_cci, fp_pk_cci; DROP FUNCTION fp_auto_pk; +-- The fast path must check EXECUTE, as the referenced table's owner, on the +-- functions it invokes on the FK values: the equality operator's function +-- and, for a cross-type FK, the implicit cast function. Validation of a new +-- constraint by a role that cannot use the bulk check (RLS is enabled on the +-- referenced table) runs per-row checks through the fast path. +-- +-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere +-- with nearby tests. +BEGIN; +CREATE ROLE regress_ri_pkowner; +CREATE ROLE regress_ri_fkowner; + +-- A private btree opclass whose equality function we can revoke without +-- affecting anything else. +CREATE FUNCTION regress_ri_int4eq(int4, int4) RETURNS bool + AS 'int4eq' LANGUAGE internal IMMUTABLE STRICT; +CREATE OPERATOR === (LEFTARG = int4, RIGHTARG = int4, + FUNCTION = regress_ri_int4eq, COMMUTATOR = ===, + RESTRICT = eqsel, JOIN = eqjoinsel); +CREATE OPERATOR CLASS regress_ri_int4_ops FOR TYPE int4 USING btree AS + OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >, + FUNCTION 1 btint4cmp(int4, int4); +REVOKE EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) TO regress_ri_fkowner; + +-- An FK column type needing a user-defined implicit cast to the PK type. +CREATE TYPE fkenum AS ENUM ('one'); +CREATE FUNCTION fkenum_to_int4(fkenum) RETURNS int4 + AS 'SELECT 1' LANGUAGE sql IMMUTABLE STRICT; +CREATE CAST (fkenum AS int4) WITH FUNCTION fkenum_to_int4(fkenum) AS IMPLICIT; +REVOKE EXECUTE ON FUNCTION fkenum_to_int4(fkenum) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION fkenum_to_int4(fkenum) TO regress_ri_fkowner; + +CREATE TABLE pktable_acl_op (a int4); +CREATE UNIQUE INDEX ON pktable_acl_op (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast (a int4 PRIMARY KEY); +CREATE TABLE pktable_acl_op_part (a int4) PARTITION BY LIST (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_op_part1 PARTITION OF pktable_acl_op_part DEFAULT; +CREATE UNIQUE INDEX ON pktable_acl_op_part (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast_part (a int4 PRIMARY KEY) PARTITION BY LIST (a); +INSERT INTO pktable_acl_op VALUES (1); +INSERT INTO pktable_acl_cast VALUES (1); +ALTER TABLE pktable_acl_op OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_op_part ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast_part ENABLE ROW LEVEL SECURITY; +GRANT REFERENCES ON pktable_acl_op, pktable_acl_cast, pktable_acl_op_part, pktable_acl_cast_part TO regress_ri_fkowner; + +CREATE TABLE fktable_acl_op (a int4); +CREATE TABLE fktable_acl_cast (a fkenum); +INSERT INTO fktable_acl_op VALUES (1); +INSERT INTO fktable_acl_cast VALUES ('one'); +ALTER TABLE fktable_acl_op OWNER TO regress_ri_fkowner; +ALTER TABLE fktable_acl_cast OWNER TO regress_ri_fkowner; + +-- The FK owner holds EXECUTE on both functions; the PK owner does not, and +-- it is the PK owner whose privileges apply. +SET ROLE regress_ri_fkowner; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op (a); -- fails +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op_part (a); -- fails (SPI path message) +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast_part (a); -- fails (SPI path message) +ROLLBACK TO SAVEPOINT s; +RESET ROLE; +ROLLBACK; + -- A STABLE cast used by an FK check must see changes made by earlier AFTER -- triggers, using the check's snapshot rather than the outer query's snapshot. -- Compare the per-row fast path with a partitioned-parent SPI check.