@@ -4485,3 +4485,267 @@ statement ok
44854485DROP ROLE parent_role
44864486
44874487subtest end
4488+
4489+ subtest triggers_with_rls
4490+
4491+ statement ok
4492+ CREATE TABLE trigger_rls_table (id INT PRIMARY KEY, value INT, owner STRING);
4493+
4494+ statement ok
4495+ INSERT INTO trigger_rls_table VALUES (1, 100, 'alice'), (2, 200, 'bob'), (3, 300, 'alice'), (4, 400, 'bob');
4496+
4497+ statement ok
4498+ CREATE USER alice;
4499+
4500+ statement ok
4501+ CREATE USER bob;
4502+
4503+ statement ok
4504+ GRANT ALL ON trigger_rls_table TO alice, bob;
4505+
4506+ # Enable RLS on the table
4507+ statement ok
4508+ ALTER TABLE trigger_rls_table ENABLE ROW LEVEL SECURITY;
4509+
4510+ # Create a policy that only allows users to see their own data.
4511+ statement ok
4512+ CREATE POLICY owner_policy ON trigger_rls_table
4513+ USING (owner = current_user())
4514+ WITH CHECK (owner = current_user());
4515+
4516+ # Create a trigger function that logs changes to the table.
4517+ statement ok
4518+ CREATE FUNCTION log_value_changes() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
4519+ BEGIN
4520+ IF TG_OP = 'UPDATE' THEN
4521+ RAISE NOTICE 'User % updated value for id % from % to %', current_user(), (NEW).id, (OLD).value, (NEW).value;
4522+ ELSIF TG_OP = 'INSERT' THEN
4523+ RAISE NOTICE 'User % inserted new row with id % and value %', current_user(), (NEW).id, (NEW).value;
4524+ ELSIF TG_OP = 'DELETE' THEN
4525+ RAISE NOTICE 'User % deleted row with id % and value %', current_user(), (OLD).id, (OLD).value;
4526+ END IF;
4527+ RETURN NEW;
4528+ END;
4529+ $$;
4530+
4531+ # Create a trigger function that enforces a business rule: value must be positive.
4532+ statement ok
4533+ CREATE FUNCTION enforce_positive_value() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
4534+ BEGIN
4535+ IF (NEW).value <= 0 THEN
4536+ RAISE EXCEPTION 'Value must be positive';
4537+ END IF;
4538+ RETURN NEW;
4539+ END;
4540+ $$;
4541+
4542+ # Create a trigger that logs changes.
4543+ statement ok
4544+ CREATE TRIGGER log_changes
4545+ AFTER INSERT OR UPDATE OR DELETE ON trigger_rls_table
4546+ FOR EACH ROW
4547+ EXECUTE FUNCTION log_value_changes();
4548+
4549+ # Create a trigger that enforces the business rule.
4550+ statement ok
4551+ CREATE TRIGGER check_positive_value
4552+ BEFORE INSERT OR UPDATE ON trigger_rls_table
4553+ FOR EACH ROW
4554+ EXECUTE FUNCTION enforce_positive_value();
4555+
4556+ # Test as alice.
4557+ statement ok
4558+ SET ROLE alice;
4559+
4560+ # Alice should only see her own rows.
4561+ query IIT
4562+ SELECT * FROM trigger_rls_table ORDER BY id;
4563+ ----
4564+ 1 100 alice
4565+ 3 300 alice
4566+
4567+ # Update a row that alice owns.
4568+ query T noticetrace
4569+ UPDATE trigger_rls_table SET value = 150 WHERE id = 1;
4570+ ----
4571+ NOTICE: User alice updated value for id 1 from 100 to 150
4572+
4573+ # Try to update a row that bob owns (should not error, but should not affect any rows).
4574+ query T noticetrace
4575+ UPDATE trigger_rls_table SET value = 250 WHERE id = 2;
4576+ ----
4577+
4578+ # Test negative value should trigger error.
4579+ statement error pq: Value must be positive
4580+ UPDATE trigger_rls_table SET value = -100 WHERE id = 1;
4581+
4582+ # Insert a new row that alice owns.
4583+ query T noticetrace
4584+ INSERT INTO trigger_rls_table VALUES (5, 500, 'alice');
4585+ ----
4586+ NOTICE: User alice inserted new row with id 5 and value 500
4587+
4588+ # Try to insert a row that bob owns (should fail due to RLS policy).
4589+ statement error pq: new row violates row-level security policy for table "trigger_rls_table"
4590+ INSERT INTO trigger_rls_table VALUES (6, 600, 'bob');
4591+
4592+ # Delete a row that alice owns.
4593+ query T noticetrace
4594+ DELETE FROM trigger_rls_table WHERE id = 3;
4595+ ----
4596+ NOTICE: User alice deleted row with id 3 and value 300
4597+
4598+ # Test as bob.
4599+ statement ok
4600+ SET ROLE bob;
4601+
4602+ # Bob should only see his own rows.
4603+ query IIT
4604+ SELECT * FROM trigger_rls_table ORDER BY id;
4605+ ----
4606+ 2 200 bob
4607+ 4 400 bob
4608+
4609+ # Update a row that bob owns.
4610+ query T noticetrace
4611+ UPDATE trigger_rls_table SET value = 250 WHERE id = 2;
4612+ ----
4613+ NOTICE: User bob updated value for id 2 from 200 to 250
4614+
4615+ # Try to update a row that alice owns (should not error, but should not affect any rows).
4616+ query T noticetrace
4617+ UPDATE trigger_rls_table SET value = 175 WHERE id = 1;
4618+ ----
4619+
4620+ # Insert a new row that bob owns.
4621+ query T noticetrace
4622+ INSERT INTO trigger_rls_table VALUES (7, 700, 'bob');
4623+ ----
4624+ NOTICE: User bob inserted new row with id 7 and value 700
4625+
4626+ # Test if bob can bypass RLS with a trigger function that modifies other rows.
4627+ statement ok
4628+ CREATE FUNCTION try_bypass_rls() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
4629+ BEGIN
4630+ UPDATE trigger_rls_table SET value = 999 WHERE owner = 'alice';
4631+ RETURN NEW;
4632+ END;
4633+ $$;
4634+
4635+ statement ok
4636+ CREATE TRIGGER bypass_attempt
4637+ AFTER INSERT ON trigger_rls_table
4638+ FOR EACH ROW
4639+ EXECUTE FUNCTION try_bypass_rls();
4640+
4641+ # Try to trigger the bypass (this insert will succeed but the trigger's UPDATE should be limited by RLS).
4642+ statement ok
4643+ INSERT INTO trigger_rls_table VALUES (8, 800, 'bob');
4644+
4645+ # Check as root what happened.
4646+ statement ok
4647+ SET ROLE root;
4648+
4649+ # Alice's rows should not have been affected by bob's trigger.
4650+ query IIT
4651+ SELECT * FROM trigger_rls_table WHERE owner = 'alice' ORDER BY id;
4652+ ----
4653+ 1 150 alice
4654+ 5 500 alice
4655+
4656+ # Now test a BEFORE trigger that modifies a column in a way that would violate an RLS policy.
4657+ statement ok
4658+ SET ROLE alice;
4659+
4660+ # Create a trigger function that tries to change the owner column to 'bob'.
4661+ statement ok
4662+ CREATE FUNCTION change_owner_to_bob() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
4663+ BEGIN
4664+ NEW.owner = 'bob';
4665+ RETURN NEW;
4666+ END;
4667+ $$;
4668+
4669+ # Create a trigger that applies the function before insert.
4670+ statement ok
4671+ CREATE TRIGGER force_bob_ownership
4672+ BEFORE INSERT ON trigger_rls_table
4673+ FOR EACH ROW
4674+ EXECUTE FUNCTION change_owner_to_bob();
4675+
4676+ # Try to insert a row - this should fail because the trigger modifies
4677+ # the owner to 'bob' which violates alice's RLS policy.
4678+ statement error pq: new row violates row-level security policy for table "trigger_rls_table"
4679+ INSERT INTO trigger_rls_table VALUES (9, 900, 'alice');
4680+
4681+ # Now do the same with update.
4682+ statement ok
4683+ CREATE FUNCTION change_updated_owner_to_bob() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
4684+ BEGIN
4685+ IF TG_OP = 'UPDATE' THEN
4686+ NEW.owner = 'bob';
4687+ END IF;
4688+ RETURN NEW;
4689+ END;
4690+ $$;
4691+
4692+ statement ok
4693+ CREATE TRIGGER force_update_bob_ownership
4694+ BEFORE UPDATE ON trigger_rls_table
4695+ FOR EACH ROW
4696+ EXECUTE FUNCTION change_updated_owner_to_bob();
4697+
4698+ # Try to update a row - this should fail because the trigger modifies
4699+ # the owner to 'bob' which violates alice's RLS policy.
4700+ statement error pq: new row violates row-level security policy for table "trigger_rls_table"
4701+ UPDATE trigger_rls_table SET value = 600 WHERE id = 5;
4702+
4703+ # Let's try with both bob and root to see how it works for them.
4704+ statement ok
4705+ SET ROLE bob;
4706+
4707+ # For bob, changing owner to bob is allowed by the policy.
4708+ query T noticetrace
4709+ INSERT INTO trigger_rls_table VALUES (10, 1000, 'alice');
4710+ ----
4711+ NOTICE: User bob inserted new row with id 10 and value 1000
4712+
4713+ # Verify the owner was changed to bob by the trigger.
4714+ query IIT
4715+ SELECT * FROM trigger_rls_table WHERE id = 10;
4716+ ----
4717+ 10 1000 bob
4718+
4719+ # For completeness, let's see from the root perspective.
4720+ statement ok
4721+ SET ROLE root;
4722+
4723+ # Check all rows in the table to see what happened.
4724+ query IIT
4725+ SELECT * FROM trigger_rls_table ORDER BY id;
4726+ ----
4727+ 1 150 alice
4728+ 2 250 bob
4729+ 4 400 bob
4730+ 5 500 alice
4731+ 7 700 bob
4732+ 8 800 bob
4733+ 10 1000 bob
4734+
4735+ # Clean up the additional triggers and functions.
4736+ statement ok
4737+ DROP TRIGGER force_bob_ownership ON trigger_rls_table;
4738+ DROP TRIGGER force_update_bob_ownership ON trigger_rls_table;
4739+ DROP FUNCTION change_owner_to_bob;
4740+ DROP FUNCTION change_updated_owner_to_bob;
4741+ DROP TRIGGER log_changes ON trigger_rls_table;
4742+ DROP TRIGGER check_positive_value ON trigger_rls_table;
4743+ DROP TRIGGER bypass_attempt ON trigger_rls_table;
4744+ DROP FUNCTION log_value_changes;
4745+ DROP FUNCTION enforce_positive_value;
4746+ DROP FUNCTION try_bypass_rls;
4747+ DROP TABLE trigger_rls_table;
4748+ DROP USER alice;
4749+ DROP USER bob;
4750+
4751+ subtest end
0 commit comments