Why does PostgreSQL permissions behave differently between Triggers and Check constraints? - postgresql

I'm currently having a Database with two schemas app_private & app_public (in addition to default public schema). I also have a role which has been granted usage on app_public schema, but not the app_private schema. I'm also using two functions (one trigger function and one check constraint function) on the table.
See below for code:
(1) Creation of the Schemas (and grants)
CREATE SCHEMA app_public;
CREATE SCHEMA app_private;
grant usage on schema public, app_public to "grant_test_role";
(2) Revoke grants from PUBLIC user
Then I'm having this special DDL statement. It is supposed to REVOKE permissions for any newly added function from the public user role (which all other roles inherit from).
alter default privileges revoke all on functions from public;
(3) Function Definitions (Trigger & Constraint)
-- Trigger Function
create OR replace function app_private.tg__timestamps() returns trigger as $$
begin
NEW.created_at = (case when TG_OP = 'INSERT' then NOW() else OLD.created_at end);
NEW.updated_at = (case when TG_OP = 'UPDATE' and OLD.updated_at >= NOW() then OLD.updated_at + interval '1 millisecond' else NOW() end);
return NEW;
end;
$$ language plpgsql volatile set search_path to pg_catalog, app_private, public, pg_temp;
-- Constraint Function
CREATE OR REPLACE FUNCTION app_private.constraint_max_length(
value text,
maxLength integer,
error_message text default 'The value "$1" is too long. It must be maximum $2 characters long.',
error_code text default 'MXLEN'
) RETURNS boolean
AS $$
begin
if length(value) > maxLength then
error_text = replace(replace(error_message, '$1', value), '$2', maxLength);
raise exception '%', error_text using errcode = error_code;
end if;
return true;
end;
$$ LANGUAGE plpgsql set search_path to pg_catalog, app_private, public, pg_temp;
(4) Table Definition (which uses above Trigger & Constraint functions)
create table app_public.test_tab (
id INT not null primary key,
name text not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint name_length_check check (app_private.constraint_max_length(name, 5));
);
create trigger _100_timestamps
before insert or update on app_public.test_tab
for each row
execute procedure app_private.tg__timestamps();
-- Setting some restrictions on the test_tab for the "grant_test_role"
REVOKE ALL ON TABLE app_public.test_tab FROM "grant_test_role";
GRANT SELECT, DELETE ON app_public.test_tab TO "grant_test_role";
GRANT
INSERT(id, name),
UPDATE(id, name) ON app_public.test_tab TO "grant_test_role";
(5) Code (which runs as grant_test_role)
begin;
set local role to grant_test_role;
insert into app_public.test_tab (id, name) values (1, 'Very Long Name');
commit;
I'm trying to execute this in a fresh DB each time for me to understand how PostgreSQL permissions work in different call contexts (i.e. Trigger Function, constraint check which calls a function automatically, etc.)
When I don't have the code block (2) which revokes functions permissions from PUBLIC user, the code block (5) executes without any errors. Event though the user role doesn't have grants to app_private schema where the trigger function and the constraint function exists. But with the code block (2) present, the code executes the trigger just fine, yet gives me a "permission denied for function constraint_max_length" for the check constraint.
So I'm trying to understand,
How does the Trigger function which exists in a schema where the user role does not have usage grants, execute successfully always?
If the trigger function executes, why does the the CHECK constraint function give me the above permission denied error?
What does the code block (2) really do?
I'm struggling a bit to find documentation about how permissions apply in this kind of "auto-executed" scenarios (triggers/constraints) since the User is not "explicitly" calling these functions, rather they are automatically called by the DB. So I'm not sure which ROLE is executing them.

I posted this question to the PostgreSQL mailing list and finally got the answer.
So as of now, this is how PostgreSQL works (be it compliant with SQL spec or not :)
Original mail thread - https://www.postgresql.org/message-id/CANYEAx8vZnN9eeFQfsiLGMi9NdCP0wUdriHTCGU-7jP0VmNKPA%40mail.gmail.com
Trigger Functions
Trigger Function privileges are checked at "creation" time against the role that creates them.
At runtime, privileges for the trigger function are not checked against the executing role at all, but simply the ability to parse.
The statements inside of the trigger function will go through usual privilege checks against the executing role
Check Constraint Functions
Check Constraint function privileges are checked at "creation" time against the role that creates them.
At runtime, the schema of the constraint function is only checked for the "ability to parse". As in if such a schema exists, but not if it is accessible.
However at runtime, the function itself (regardless of the schema it exists in) is checked for privileges against the executing role.
So this explains the behavior I was encountering in PostgreSQL

Related

How to grant permissions / set role with ddl_command_start event trigger without explicit sql execution?

I encountered an issue while resetting the table owner whenever an index is created on table in psql. Am doing this programatically by defining ddl_command_start.
Flow of events:
On /create api execution, backend creates a table using role table_role and resets the owner to superuser/admin
On /createIndex api execution ,backend tries to create index using role table_role , in doing so it cannot execute ddl_command_start as table_role is not the owner of created table in step1.
Is there a way to grant temporary access to the created table as owner in second step ? ( without executing an explicit sql from code )
Is it possible to invoke db function or other means to reset the role before permission check happens on a table ?
Please let me know your comments. Thank you.
permForIndexEndFunc := `CREATE OR REPLACE FUNCTION internal.trg_create_index_set_owner()
RETURNS event_trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
obj record;
tablename text;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag='CREATE INDEX' LOOP
tablename := substring(obj.object_identity from 1 for 10);
EXECUTE format('SET ROLE %s','admin');
EXECUTE format('ALTER TABLE %s OWNER TO table_role', tablename);
END LOOP;
END;
$$;`
_, err = db.Exec(permForIndexEndFunc)
if err != nil {
return err
}
permForIndexEndTrigger := `CREATE EVENT TRIGGER trg_create_index_set_owner
ON ddl_command_start
WHEN tag IN ('CREATE INDEX')
EXECUTE PROCEDURE internal.trg_create_index_set_owner();`
_, err = db.Exec(permForIndexEndTrigger)
if err != nil {
return err
}
Basically my app creates generic ingestion db schemas based on client configuration (either through grpc / rest api calls),so customer would send the schema in the form of json & it creates the identical db schema in backend , then onwords customer sends data in the pre-defined json format.So basically when schema is created on db event triggers maintain the rbac of so and so schema,tables.Schema,table creation itself happens by assuming creation only role by resetting owner to superuser using event trigger, so by the time index creation owner of table has been reset & fail.
Let's call the superuser/admin that owns the table from step 1 “user a”.
Then, for table_role to be allowed to create an index on the table, it must be a member of a. So you have to grant role membership temporarily:
BEGIN;
GRANT a TO table_role;
CREATE INDEX ...;
REVOKE a FROM table_role;
COMMIT;
Now if a is really a superuser, then of course table_role is not allowed to do that unless it is also a superuser (but in that case, the problem wouldn't exist, because superusers are allowed to do anything).
So make sure that a is no superuser. Then table_role needs to have CREATEUSER to be allowed to do tha above.

Can event triggers be used to alter the owner of a newly created table to a role the creator is not a member of?

I want to alter the owner of a postgreSQL table upon creation. So far so good.
CREATE OR REPLACE FUNCTION trg_create_set_table_owner()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag='CREATE TABLE' LOOP
EXECUTE format('ALTER TABLE %s OWNER TO blubb', obj.object_identity);
END LOOP;
END;
$$;
However, I would like to change the owner to a role that the user creating the table is not a part of. Is that possible at all?
https://www.postgresql.org/docs/current/static/sql-createfunction.html#SECURITY DEFINER
Because a SECURITY DEFINER function is executed with the privileges of
the user that owns it, care is needed to ensure that the function
cannot be misused
Apart of this - just create a function with SU that will accept tablename and rolename as arguments and will run ALTER TABLE. grant execute to the role, consuming trg_create_set_table_owner and it will work. But the concern that the role can use the ability to alter table outside of trg_create_set_table_owner remains...
Of course you can check the possible role names and table names from a list of allowed before passing them to execute format('ALTER TABLE %I OWNER TO %I')

Prevent query assigning ID when column has a default

I have a table that has a column defined as id serial. Therefore, it has a default on the column to call a sequencer. Which is what I want. However, my boss (and owner of the company) insists on having full access to the database and likes to write his own queries (including inserts) on this table. And even more unfortunately, he seems to always forgets that these IDs should be auto-generated for him, and he creates the record with max(id) + 1. Then the sequencer is out of sync with the table and the next time someone calls the API we get a 500.
Question: Given that I don't have the ability to lock him out of the database, what is the best way to prevent him from doing this?
My first idea was create a before insert trigger and raise an error if the ID was assigned. Here is what I did:
CREATE OR REPLACE FUNCTION md.tf_no_manual_keys()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
IF NEW.id IS NOT NULL THEN
RAISE EXCEPTION 'ID must be assigned from sequencer';
END IF;
END;
$function$
trigger_no_manual_keys BEFORE INSERT ON vendor FOR EACH ROW EXECUTE PROCEDURE tf_no_manual_keys()
But, this doesn't work because the sequencer has already fired (I didn't think that was the case for a "BEFORE" trigger).
I guess I could always call the trigger on BEFORE INSERT, but that seems like it would use 2 IDs whenever it's called.
I'm using PostgreSQL 9.4
If you have grant privilege on the table you can revoke a role's insert privilege on a certain column:
revoke insert (id) on table t from role_name;
But that will not work if that role is a member of another role that has that privilege.

Prevent update of column except by trigger

Is it possible to configure a Postgres database such that a specific column may only be updated by a trigger, while still allowing the trigger itself to be executed in response to an update by a role without permission to update that column? If so, how?
For example, given tables and a trigger like this:
CREATE TABLE a(
id serial PRIMARY KEY,
flag boolean NOT NULL DEFAULT TRUE,
data text NOT NULL
);
CREATE TABLE b(
id serial PRIMARY KEY,
updated_on DATE NOT NULL DEFAULT CURRENT_DATE,
a_id INTEGER NOT NULL,
FOREIGN KEY (a_id) references a(id)
);
CREATE FUNCTION update_aflag() RETURNS trigger AS $update_aflag$
BEGIN
UPDATE a
SET flag = FALSE
WHERE id = NEW.a_id;
RETURN NEW;
END;
$update_aflag$ LANGUAGE plpgsql;
CREATE TRIGGER update_aflag_trigger
AFTER INSERT ON b
FOR EACH ROW
EXECUTE PROCEDURE update_aflag()
;
I'd like to define a role which does not have permission to update a.flag directly using an UPDATE statement, but which may update flag indirectly via the trigger.
Yes, this is possible using a SECURITY DEFINER trigger function. The trigger function runs as a role that has the right to modify the flag column, but you don't GRANT that right to normal users. You should create the trigger function as the role that you'll grant the required rights to.
This requires that the application not run as the user that owns the tables, and of course not as a superuser.
You can GRANT column update rights to other columns to the user, just leave out the flag column. Note that GRANTing UPDATE on all columns then REVOKEing it on flag will not work, it's not the same thing.

Allow some roles to update a column, and some others if it IS NULL

I have a column A (type int) in a table for which data is not available at insert time of the other values. I do not want to split the table, as there is no other real reason to do so. I am implementing privilege separation at the database level.
Only certain users (who belong to category A) should be able to modify column A at any time. But other users (in category B, not necessarily mutually exclusive to category A) should be able to update the column if its value is not set already, ie. if it is NULL.
I am using PostgreSQL 9.2.4.
How can I accomplish this? Triggers? Rules? Something else?
only certain users should be able to modify column A
...
other users should be able to update the column if ... it is NULL.
Revoke UPDATE (and DELETE ?!?) from public and everybody else who should not have the privilege.
REVOKE UPDATE ON TABLE tbl FROM public;
REVOKE UPDATE ON TABLE tbl FROM ...
Create a (group-)role some_users that is allowed to update col_a (and nothing else):
CREATE ROLE some_users;
GRANT SELECT, UPDATE (col_a) ON TABLE tbl TO some_users;
Why the SELECT? The manual on GRANT:
In practice, any nontrivial UPDATE command will require SELECT
privilege as well, since it must reference table columns to determine
which rows to update, and/or to compute new values for columns.
This way, you have a single point where you dole out privileges for the column.
Create another (group-)role certain_users that can do everything some_users can (plus some more):
CREATE ROLE certain_users;
GRANT some_users TO certain_users;
Grant membership in these roles to user-roles as needed:
GRANT some_users TO lowly_user;
GRANT certain_users TO chief_user;
Create a conditional trigger, similar to what #Daniel provided, but with another condition using pg_has_role():
CREATE TRIGGER tbl_value_trigger
BEFORE UPDATE ON tbl
FOR EACH ROW
WHEN (OLD.col_a IS NOT NULL AND NOT pg_has_role('some_users', 'member'))
EXECUTE PROCEDURE always_fail();
Using the trigger function:
CREATE FUNCTION always_fail()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
-- To fail with exception, rolling back the whole transaction
-- use this instead:
-- RAISE EXCEPTION 'value is not null';
-- Do nothing instead, letting the rest of the transaction commit.
-- Requires BEFORE trigger.
RAISE WARNING 'col_a IS NOT NULL. User % is too ambitious!', current_user;
RETURN NULL; -- effectively aborts UPDATE
END
$func$;
Now:
The general public is restricted from updating the table completely.
some_users and certain_users are allowed to update the column col_a (and nothing else),
Only changes from certain_users go through once col_a is NOT NULL.
Note that superusers are member of any group automatically. So the form NOT pg_has_role(...) enables superusers, while an inverse logic would potentially prohibit superusers from updating the column.
An update trigger should work. It would need to check the old value and abort the update if it is not NULL.
Example:
-- setup
CREATE TABLE tbl (tbl_id serial PRIMARY KEY, value int);
-- trigger function that always fails
CREATE FUNCTION always_fail() RETURNS trigger as $$ begin raise exception 'value is not null'; end; $$ LANGUAGE plpgsql;
-- create the trigger
CREATE TRIGGER tbl_value_trigger BEFORE UPDATE ON tbl FOR EACH ROW WHEN (old.value is not null) EXECUTE PROCEDURE always_fail();
-- test it
INSERT INTO tbl(value) VALUES (NULL);
INSERT INTO tbl(value) VALUES (NULL);
UPDATE tbl SET value=2 WHERE tbl_id=1;
UPDATE tbl SET value=3 WHERE tbl_id=1; -- fails
UPDATE tbl SET value=2 WHERE tbl_id=2;
UPDATE tbl SET value=3 WHERE tbl_id=2; -- fails
-- cleanup
DROP TABLE tbl;
DROP FUNCTION always_fail();
Notes:
I have to create a trigger function (CREATE FUNCTION) because CREATE TRIGGER expects a trigger function and does not accept a normal function, so using CREATE TRIGGER ... EXECUTE PROCEDURE raise exception 'value not null' is not possible
The trigger is evaluated before each update, i.e.
INSERTS are not checked, so you can insert NULLs
the check happens before the update, so it can be aborted
I have to use FOR EACH ROW, otherwise I'd be unable to check the OLD.value
To grant the unconditional update privilege to some users make they members of a role with that privilege.
create role the_role;
grant update on table t to the_role;
grant the_role to the_user;
Now this update query will only work if the current user is member of the_role or if the value is null:
update t
set A = 1
where
id = 1
and (
A is null
or
pg_has_role('the_role', 'usage')
)
Access privileges functions
Database Roles