Prevent query assigning ID when column has a default - postgresql

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.

Related

How do I create a temporary trigger in Postgres? [duplicate]

This question already has an answer here:
Drop trigger/function at end of session in PostgreSQL?
(1 answer)
Closed last month.
I'm trying to create a system in Postgres where each client can create its own subscriptions via listen + notify + triggers. I.e. the client will listen to a channel, and then create a trigger which runs notify on that channel when its conditions have been met. The issue is that I want Postgres to clean up properly in case of improper client termination (e.g. the remote client process dies). To be more specific, I want that trigger which is calling the notify to be removed as there is no longer a listener anyways. How can I accomplish this?
I've thought about having a table to map triggers to client ids and then using that to remove triggers where the client is gone, but it seems like a not so great solution.
I found an answer to this in another question: Drop trigger/function at end of session in PostgreSQL?
In reasonably recent Postgres versions you can create a function in
pg_temp schema:
create function pg_temp.get_true() returns boolean language sql as $$ select true; $$;
select pg_temp.get_true();
This is the schema in which temporary tables are created. All its
contents, including your function, will be deleted on end of session.
You can also create triggers using temporary functions on tables. I've
just tested this and it works as expected:
create function pg_temp.ignore_writes() returns trigger language plpgsql as $$
begin
return NULL;
end;
$$;
create table test (id int);
create trigger test_ignore_writes
before insert, update, delete on test
for each row execute procedure pg_temp.ignore_writes();
Because this trigger function always returns NULL and is before [event] it should make any writes to this table to be ignored. And
indeed:
insert into test values(1);
select count(*) from test;
count
-------
0
But after logout and login this function and the trigger would not be
present anymore, so writes would work:
insert into test values(1);
select count(*) from test;
count
-------
1
But you should be aware that this is somewhat hackish — not often used
and might not be very thoroughly tested.
That's not how it works. CREATE TRIGGER requires that you either own the table or have the TRIGGER privilege on it (which nobody in their right mind will give you, because it enables you to run arbitrary code in their name). Moreover, CREATE TRIGGER requires an SHARE ROW EXCLUSIVE lock on the table and DROP TRIGGER requires an ACCESS EXCLUSIVE lock, which can be disruptive.
Create a single trigger and keep that around.

Postgres: raise exception from trigger if column is in INSERT or UPDATE satement

I want to audit created_by, created_timestamp, modified_by, and modified_timestamp columns in my PostgreSQL table with triggers. Creating BEFORE INSERT and BEFORE UPDATE triggers to set these values to current_user and now() is reasonably straightforward.
However, if someone tries to do:
INSERT INTO SOMETABLE(someColumn, created_by) VALUES ('test', 'someOtherUser');
I'd rather throw an exception like, 'Manually setting created_by in an INSERT query is not allowed." instead of having the trigger silently change 'someOtherUser' to current_user.
I thought I could accomplish this in the trigger with:
if new.created_by is not null then raise exception 'Manually setting created_by in an INSERT query is not allowed.'; end if;
This works as expected for INSERT queries and triggers.
However, using the same strategy for UPDATE triggers, I'm finding it a bit more difficult, because the NEW record has the unchanged values from the existing row in addition to the changed values in the UPDATE query. (At least, I think that's what's happening.)
I can compare new.created_by to old.created_by to ensure they're the same, thus preventing the query from changing the value, but even though the end result is similar (i.e. the value in the table doesn't get changed), this really isn't the same as disallowing the column from being in the UPDATE query at all.
Is there an elegant way to determine if a column is present in the INSERT or UPDATE query? I've seen some suggestions here to convert to JSON and test that way, but that seems to be a rather ugly solution to me.
Are there other solutions to ensurevthese columns (created_by, created_timestamp, etc.) are only set by the trigger functions and are not manually settable in INSERT and UPDATE queries?
Create a special trigger for UPDATE with a name that is early in the alphabet, so that it is called before your other trigger:
CREATE FUNCTION yell() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
RAISE EXCEPTION 'direct update of "created_by" is forbidden';
END;$$;
CREATE TRIGGER aa_nosuchupdate
BEFORE UPDATE OF created_by FOR EACH ROW
EXECUTE PROCEDURE yell();
The INSERT case can be handled in your other trigger.

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

Create non conflicting temporary tables in a Pl/pgSQL function

I want to create a TEMPORARY TABLE in a Pl/pgSQL function because I want to index it before doing some process. The fact that any concurrent call to the function will try to reuse the same table seems to be a problem.
e.g. A first call to the function creates and uses a temporary table named "test" with data depending on the function parameters. A second concurrent call tries also to create and use the temporary table with the same name but with different data...
The doc says
"Temporary tables are automatically dropped at the end of a session,
or optionally at the end of the current transaction"
I guess the problem would not exist if temporary tables created with the "ON COMMIT DROP" option would only be visible to the current transaction. Is this the case?
If not, how to automatically create independent tables from two different function calls?
I could probably try to create a temporary name and check if a table with this name already exists but that seems like a lot of management to me...
Temporary tables of distinct sessions cannot conflict because each session has a dedicated temporary schema, only visible to the current session.
In current Postgres only one transaction runs inside the same session at a time. So only two successive calls in the same session can see the same temporary objects. ON COMMIT DROP, like you found, limits the lifespan of temp tables to the current transaction, avoiding conflicts with other transactions.
If you (can) have temp tables that don't die with the transaction (like if you want to keep using some of those tables after the end of the current transaction), then an alternative approach would be to truncate instead of create if the temp table already exists - which is a bit cheaper, too.
Wrapped into a function:
CREATE OR REPLACE FUNCTION f_create_or_trunc_temp_table(_tbl text, OUT _result "char") AS
$func$
BEGIN
SELECT INTO _result relkind
FROM pg_catalog.pg_class
WHERE relnamespace = pg_my_temp_schema() -- only temp objects!
AND relname = _tbl;
IF NOT FOUND THEN -- not found
EXECUTE format('CREATE TEMP TABLE %I(id int)', _tbl);
ELSIF _result = 'r' THEN -- table exists
EXECUTE format('TRUNCATE TABLE %I', _tbl); -- assuming identical table definition
ELSE -- other temp object occupies name
RAISE EXCEPTION 'Other temp object of type >>%<< occupies name >>%<<', _result, _tbl;
-- or do nothing, return more info or raise a warning / notice instead of an exception
END IF;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT f_create_or_trunc_temp_table('my_tbl');
This assumes identical table definition if the table exists. You might do more and also return more informative messages, etc. This is just the basic concept.
Related:
How can I determine if a table exists in the current search_path with PLPGSQL?
How to check if a table exists in a given schema
Temporary tables are visible only in the current session. Concurrent processes do not see each other's temporary tables even when they share the same names. Per the documentation:
PostgreSQL requires each session to issue its own CREATE TEMPORARY TABLE command for each temporary table to be used. This allows different sessions to use the same temporary table name for different purposes (...)

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