Prevent update of column except by trigger - postgresql

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.

Related

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

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

Immutability in Postgres

I want to create an immutable Postgres database, where the user can insert & select (write & read) data, but cannot update or delete the data.
I am aware of the FOR UPDATE lock, but I don't understand how to use it.
Let's say for example I have the table below, how can I make it immutable (or, if I understood correctly, how can I use the FOR UPDATE lock permanently)
CREATE TABLE account(
user_id serial PRIMARY KEY,
username VARCHAR (50) UNIQUE NOT NULL,
password VARCHAR (50) NOT NULL,
email VARCHAR (355) UNIQUE NOT NULL,
created_on TIMESTAMP NOT NULL,
last_login TIMESTAMP
);
The solution is to give the user that accesses the database only the INSERT and SELECT privilege on the tables involved.
A lock is not a tool to deny somebody access, but a short-time barrier to prevent conflicting data modifications to happen at the same time.
Here is an example:
CREATE TABLE sensitive (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
available text,
restricted text
);
Now I want to allow someuser to insert data and read and update all columns except restricted, and I want to keep myself from deleting data in that table:
/* the CREATE TABLE above was run by user "laurenz" */
REVOKE DELETE ON sensitive FROM laurenz;
GRANT INSERT ON sensitive TO someuser;
GRANT SELECT (id, available), UPDATE (id, available) ON sensitive TO someuser;
Nope, that 👆🏼 solution doesn't work. I found this one. I make a before trigger on the table on update for each row:
create or replace function table_update_guard() returns trigger
language plpgsql immutable parallel safe cost 1 as $body$
begin
raise exception
'trigger %: updating is prohibited for %.%',
tg_name, tg_table_schema, tg_table_name
using errcode = 'restrict_violation';
return null;
end;
$body$;
create or replace trigger account_update_guard
before update on account for each row
execute function table_update_guard();
See my original research.

Prevent updates to generated primary key column

In PostgreSQL I have created a table and with an id column defined as serial. I have inserted two rows, but I can still update the value of the id column.
But I need prevent updates to the generated value of the id column.
create table aas.apa_testtable
(
id serial primary key,
name text
)
insert into aas.apa_testtable(name) select ('test')
insert into aas.apa_testtable(name) select ('test2')
-- I want this to be impossible / result in an error:
update aas.apa_testtable set id=3 where id=2
You can revoke update on table and grant it on column(s):
REVOKE UPDATE ON TABLE aas.apa_testtable FROM some_role;
GRANT UPDATE (name) ON TABLE aas.apa_testtable TO some_role;
Remember about role public, superusers and other inheritance issues you might have in your setup.
--Do not try this, it will not work without revoking table level privileges:
REVOKE UPDATE (id) ON TABLE aas.apa_testtable FROM some_role;
Alternative is to create trigger that will check if old != new, but with details provided I don't see need for it.

How to use the auto-generated primary key in the same row postrges

I am trying to make an insert statement into a table with the following schema
CREATE TABLE auth(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role VARCHAR(64)
);
I am using pgcrypto extension to generate uuids. Is it possible to append id to the role field while inserting a row in this table?
I am inserting in this table using
insert into auth (role) values ('admin');
I want to append the id generated to admin so that the role would look something like admin_12234-3453-3453-345-34534.
You need an insert trigger to do this:
CREATE FUNCTION admin_uuid() RETURNS trigger AS $$
BEGIN
NEW.role := NEW.role || NEW.id::text;
RETURN NEW;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE TRIGGER set_admin_uuid
BEFORE INSERT ON auth
FOR EACH ROW EXECUTE PROCEDURE admin_uuid();
I think there is no direct way to achieve this. But postgres allows defining rules on tables and views (see https://www.postgresql.org/docs/9.2/static/sql-createrule.html). You can create a new rule for insertion which creates the id and the role as desired.

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