Postgres: How to prevent anyone from updating a table? - sql

I've been trying to stop any user from updating a table
I tried
REVOKE UPDATE ON TABLE NYAidCrimeMean FROM CURRENT_USER;
but when I try to update the table it still goes through, any pointers?

Well, I do not particular like triggers, but they have their uses. If you cannot find all the previous grants, you can get close here with one. See demo.
create or replace function no_no_no()
returns trigger
language plpgsql
as $$
begin
raise exception 'Cannot update table %s.', tg_table_name;
return null;
end;
$$;
create trigger No_no_no_NYAidCrimeMeanA
before update on NYAidCrimeMean
for each statement
execute function no_no_no();
If you really want to lock it down change before update to before insert or update or delete or truncate.
Of course a superuser can disable the trigger, then do whatever they want, but eventually get to where you must trust someone at some point.

The solution is as trivial as it is obvious: don't use superusers for anything but administative tasks.

Related

Postgres: Update table sorting on new record inserted or removed

I'm trying to sort table automatically by specified row each time a new record is added (or removed or updated).
For that, I've create a function
CREATE FUNCTION pid_cluster_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
-- trigger logic
cluster verbose public.pid using pid_idx;
END;
$$
and add a trigger
CREATE trigger pid_cluster_trigger
after INSERT or update or DELETE on public.pid
FOR EACH row
execute procedure pid_cluster_function();
but with adding a record
INSERT INTO public.pid (pid,pid_name) VALUES ('111','new 111');
I've received such an error
SQL Error [55006]: ERROR: cannot CLUSTER "pid" because it is being used by active queries in this session
Where: SQL statement "cluster verbose public.pid using pid_idx"
PL/pgSQL function pid_cluster_function() line 5 at SQL statement
What is the reason for this error?
Or is it possible to achieve sorting by adding or modifying the records in some other way?
Ok, thank you, everyone, in the comments. I see that my idea is not clever =)

How transactions work in case of Postgres Functions

I am trying to understand how transaction works in Postgres and what happens when multiple commands try to work on the same table. My doubt is related to a small experiment that I carried out.
Consider a table called experiment with a trigger (experiment_log) on it that is fired after every update, delete, or insert.
Now consider this function.
CREATE OR REPLACE FUNCTION test_func() RETURNS void AS $body$
DECLARE
_q_txt text;
version_var integer;
BEGIN
EXECUTE 'DROP TRIGGER IF EXISTS experiment_log ON experiment';
SELECT version INTO version_var FROM experiment;
RAISE NOTICE 'VERSION AFTER DROPPING TRIGGER: %', version_var;
EXECUTE 'SELECT pg_sleep(20);';
SELECT version INTO version_var FROM experiment;
RAISE NOTICE 'VERSION BEFORE RECREATING TRIGGER: %', version_var;
EXECUTE 'CREATE TRIGGER experiment_log AFTER INSERT OR UPDATE OR DELETE ON experiment FOR EACH ROW EXECUTE PROCEDURE experiment_log_trigger_func();';
END;
$body$
language 'plpgsql';
So, this function drops the trigger and waits for 20 seconds before re-creating this trigger. Now any update operation performed during the time when function is sleeping, the update operation blocks. It means that I can not update the experiment table until the function test_func has executed completely.
Can anyone explain this behaviour? It seems I am missing something out to reason this behaviour.
That is because DROP TRIGGER places an ACCESS EXCLUSIVE lock on the table, and the lock is held until the transaction ends, that is, for the whole duration of the function call.
If you want to disable a trigger temporarily, use
ALTER TABLE experiment DISABLE TRIGGER experiment_log;
I would like to give you a reference from the documentation, but the lock level of DROP TRIGGER is not documented. However, it is documented that the SQL statement takes the lock:
Also, most PostgreSQL commands automatically acquire locks of appropriate modes to ensure that referenced tables are not dropped or modified in incompatible ways while the command executes.
There you can also find how long a lock is held:
Once acquired, a lock is normally held until the end of the transaction.
To find the lock taken by DROP TRIGGER, try this simple experiment:
CREATE TABLE t();
CREATE TRIGGER whatever BEFORE UPDATE ON t
FOR EACH ROW EXECUTE FUNCTION suppress_redundant_updates_trigger();
BEGIN; -- start a transaction
DROP TRIGGER whatever ON t;
SELECT mode FROM pg_locks
WHERE pid = pg_backend_pid() -- only locks for the current session
AND relation = 't'::regclass; -- only locks on "t"
mode
═════════════════════
AccessShareLock
AccessExclusiveLock
(2 rows)
COMMIT;
You see that an ACCESS SHARE lock and an ACCESS EXCLUSIVE lock are held on the table.

How to restrict a user from truncating a table when a long transaction is occurring?

There is a refresh materialized concurrently that takes several hours to run. One of the users regularly has to truncate one of the tables that the materialized view uses. This table is also used in multiple projects so other users run selects on it
Problem is that this truncate stays locked until the refresh finishes, and anyone selecting the table then gets completely stuck and then the database gets jammed. I've instructed the user to only do this truncate at a specific time but he did not listen
How to create a trigger that prevents the user from doing this truncate? Something along the lines
create trigger before truncate on table for each row execute function stoptruncate()
CREATE OR REPLACE FUNCTION stoptruncate()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
if truncate = true and if 'the refresh query is running'
then raise exception 'cannot run truncate while background refresh is running';
end if;
RETURN NEW;
END;
$function$
;
A trigger comes too late to do anything about that. The trigger function is called after the table lock has been granted.
You could keep the user from waiting forever by setting a low default value for lock_timeout for that user:
ALTER ROLE trunc_user SET lock_timeout = '200ms';

Understanding the BEFORE UPDATE trigger in SQL

In PostgreSQL, if I want to make changes on the UPDATE command, apparently I should trigger on BEFORE UPDATE. For example:
CREATE OR REPLACE FUNCTION trigger_update_fn() RETURNS trigger AS
$$ BEGIN
new.updated := current_timestamp;
new.created := old.created; -- override changes with original value
RETURN new;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update
BEFORE UPDATE ON data
FOR EACH ROW
EXECUTE PROCEDURE trigger_update_fn();
In SQL Server there isn’t a BEFORE UPDATE trigger, and you make do with an AFTER UPDATE trigger. The point is that it seems to me that the trigger should work if it’s applied after the update in PostgreSQL, but it doesn’t, of course.
The question is why is the correct trigger a BEFORE UPDATE trigger, and what should I use the AFTER UPDATE trigger for?
The BEFORE trigger can modify the row about to be inserted into the table (NEW). The AFTER trigger runs after the table has been modified, so any changes to NEW have no effect on the persisted data.

Exception to handle unpermitted DML-operation

I'm trying to hinder a user who is not me from performing DML-operations on a table. I'm using a trigger but have some issues with the syntax. It is the "if user is not 'me' part that troubles me. Also, is stating "rollback;" enough to undo the operation? Thanks!
create or replace trigger only_me_ex
before insert or update or delete on table
for each row
declare
only_me_ex exception;
begin
if user is not 'me' then
raise only_boss exception;
end if;
exception
when only_me_ex then
raise_appication_error(-200001,'Not permitted!');
rollback;
end;
/
In your code you use 'USER' keyword, which is actually schema from which is operation performed. E.g. SCOTT is owner of emp table. If you log in as SCOTT an perform update on emp table, the USER as used in your trigger is SCOTT. If you log in as SYS and perform DML on table USER will be SYS. To sum it up:
You don't need trigger like this, you need to grant insert, update, delete privileges on this table only to those users who ought to be allowed to.
In fact rather than schema(user) you may need to know operation system user:
SYS_CONTEXT('USERENV','OS_USER')
EDIT: based on your comment that this is for academic purpose I made changes to your trigger, so it now compiles and works (I stick to your method of declaring exception, raising it and handle it as rerising an application error, which doesn't make much sense to me, but nevermind)
create table my_table (id number)
/
create or replace trigger only_me_ex
before insert or update or delete on my_table
declare
only_me_ex exception;
begin
if user!='TESTUSER' then
raise only_me_ex ;
end if;
exception
when only_me_ex then
raise_application_error(-20001,'Not permitted!');
end;
/
Note that I changed your trigger from level row trigger to statement trigger because it needs to be executed only once and I omitted rollback keyword which is not needed because there will not be anything to rollback (unless you want to rollback some previous operation in transaction);
if user not in ('testuser') then raise not_a_chance;
solved it