I’m looking for a confirmation and possible explanation.
In PostgreSQL, I create a table A, referencing table B by FK with constrain option ON DELETE UPDATE SET DEFAULT
. If the referenced record from table B is deleted, the physical "change" of the original FK for the default value is apparently done by the "system role" (in my case postgres)
Now, I have certain other triggers set on table A, that prevent editing when my "is_locked" column is set to True
. Based on the described role switching, I expected that I can let the change happen (even with is_locker==True
if current_role = 'postgres'
.
However this only turned to work when the trigger is "BEFORE UPDATE" and not "AFTER UPDATE". in other words, based on my observations, the moment the records gets physically updated, the db changes the executing role back to the original role (the one that deleted the record in table B).
Is this something expected? If so, how do you think I should approach the "allow edits if role is postgres" even after the record is inserted?
A code to replicate and observe:
BEGIN;
CREATE TABLE parent (id INT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE child (
id INT PRIMARY KEY,
parent_id INT DEFAULT 0,
edited_by text,
CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES parent (id) ON DELETE SET DEFAULT
);
CREATE TABLE logs ("text" text);
CREATE OR REPLACE FUNCTION react_on_child_update () RETURNS TRIGGER AS $$
BEGIN
INSERT INTO logs ("text") VALUES (CURRENT_role::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_before_react_on_child_update BEFORE
UPDATE ON child FOR EACH ROW
EXECUTE FUNCTION react_on_child_update();
CREATE TRIGGER trigger_after_react_on_child_update
AFTER UPDATE ON child FOR EACH ROW
EXECUTE FUNCTION react_on_child_update();
INSERT INTO parent (id, name) VALUES (0, 'ghost');
INSERT INTO parent (id, name) VALUES (1, 'Parent 1');
INSERT INTO parent (id, name) VALUES (2, 'Parent 2');
INSERT INTO child (id, parent_id) VALUES (1, 1);
INSERT INTO child (id, parent_id) VALUES (2, 2);
CREATE ROLE "user";
GRANT ALL ON TABLE parent,logs,child TO "user";
SET ROLE TO "user";
DELETE FROM parent
WHERE id = 1;
SELECT * FROM parent;
SELECT * FROM child;
-- See that first role was the admin one, second the caller ("user")
SELECT * FROM logs;
Thanks a lot!
2
Answers
I replicated your case study and got postgres as the credential in the update according to your commands. But that’s because I’m logged to the data base as such. Not sure I understand the purpose of this test.
The owner of the constraint, which by extension has to also be the owner of the table it’s on. There’s no "system role", things are just owned by regular roles and things they set up can be traced back to them.
I’m guessing the role
postgres
just owns that table. Ifsomeone_else
owned it, you’d see the update being run by them instead –postgres
isn’t special, it just happens to be their table and their constraint. You can check it inpg_tables
:It shouldn’t matter. If you check the
current_user
and based on thatraise exception
in theafter
trigger, this will cause the entire chain of events to be invalidated just as well as if it was thrownbefore
,after
orinstead of
.As to the visibility: even if you set the
before
trigger function tosecurity invoker
, theupdate
caused by referential integrity constraintcascade
action is issued as the owner of that constraint, so the function is also invoked by them. That’s whatcurrent_role
shows both in the context oftrigger..when()
as well as inside the function that it proceeds to execute:demo at db-fiddle
In order to see who caused that cascading update to run from inside the trigger it fired, you could check the
session_user
and use that to allow/reject thatupdate
– that remains untouched even for theupdate
resulting fromcascade
.Problem is, the owner that operates under
set role not_the_owner
, would still get identified as the owner. And vice versa: someone connected as not-the-owner but switched to owner mid-session, still gets rejected unless they reconnect as the owner.The
create constraint trigger..after update
idea should work fine (here‘suser
failing to remove theparent
, here‘s the owner succeeding). Withsecurity invoker
(default), the function will run under thecurrent_user
role and from there, you canraise
to prevent the cascaded update, or run a different one if that’s what you want. Make sure to check new/old values and compare them to the default insidewhen()
to avoid falling in a loop where this newupdate
triggers anotherupdate
and so on.