Setting local config in SQL before INSERT - sql

newbie in SQL coming from a JS world needing some advice on triggers.
My user has an id column and my GraphQL API always calls INSERT INTO .... RETURNING * and then doing the transforms on the GraphQL layer to return what I want.
The goal is to allow a query like INSERT INTO .... RETURNING * work with RLS in place.
The policies are:
CREATE POLICY USER_SELECT_RESTRICTIONS
ON "user"
FOR SELECT
USING ("id" = current_user_id() OR current_user_role() = 'admin' OR current_user_role() = 'partner');
CREATE POLICY USER_INSERT_RESTRICTIONS
ON "user"
FOR INSERT
WITH CHECK (true);
This breaks for guest users (more context at the bottom) because they are allowed to INSERT but cannot SELECT (because of the restriction that only authorized users can select their own rows).
So I had the idea to somehow set the user setting manually in a trigger before/after insert (don't know which because I couldn't move forward due to the syntax error).
I've tried this but I get a syntax error at the NEW.id? (it just says "Syntax Error" to me)
CREATE OR REPLACE FUNCTION after_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.userId NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_user_id_on_insert
AFTER INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE after_user_insert();
Searched around a lot and to be honest I think I may just not find anything because I am missing the correct terminology to find what I need to find.
Would appreciate not only help on what on this specific problem but also any related advice on policies for guest users that need priviliges.
Context:
These are the relevant tables and functions
CREATE TYPE "user_role" AS ENUM (
'customer',
'partner',
'admin'
);
​
CREATE TABLE "user" (
"id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
"first_name" varchar NOT NULL,
"last_name" varchar NOT NULL,
"email" text UNIQUE NOT NULL,
"role" user_role NOT NULL DEFAULT 'customer',
"created_at" timestamp NOT NULL DEFAULT NOW(),
"updated_at" timestamp NOT NULL DEFAULT NOW()
);
​
CREATE FUNCTION current_user_id() RETURNS uuid AS $$
SELECT nullif(current_setting('jwt.claims.userId', true), '')::uuid;
$$ LANGUAGE SQL stable;
​
CREATE FUNCTION current_user_role() RETURNS user_role AS $$
SELECT nullif(current_setting('jwt.claims.role', true), '')::user_role;
$$ LANGUAGE SQL stable
The RLS restricts SELECT to rows where the id column of the user table matches current_setting('jwt.claims.userId').
This was set previously by Postgraphile as seen here (https://www.graphile.org/postgraphile/security/).
One possible workaround I thought of would be this but I don't know if this would lead to some kind of vulnerabilities because of the obvious role elevation:
CREATE OR REPLACE FUNCTION after_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.role TO "customer";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION before_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.role TO "admin";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_insert
BEFORE INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE before_user_insert();
CREATE TRIGGER after_insert
AFTER INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE after_user_insert();

Your don't say what a guest is, but your approach seems wrong.
Rather than disabling your checks on a low level, which gives you a bad feeling for good reasons, you should choose one of the following approaches:
fix your policies to allow the necessary operation (perhaps by adding a permissive policy)
have certain operations performed by a SECURITY DEFINER function that belongs to a user not subject to the restrictions.
If you insist on a trigger based solution, you have to use a BEFORE trigger. Also, consider that you cannot use parameterss with a SET statement. You'd either have to use dynamic SQL or (better) use a function:
CREATE OR REPLACE FUNCTION before_user_insert() RETURNS TRIGGER AS
$$BEGIN
SELECT set_config('jwt.claims.userId', NEW.id::text, TRUE);
RETURN NEW;
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER set_user_id_on_insert BEFORE INSERT ON "user"
FOR EACH ROW EXECUTE PROCEDURE before_user_insert();

Related

PostgreSQL unable to INSERT INTO

I want to reflect info from one table into another one. Ideally, I would need to use a function (will be upgrading afterwards). I have a first table called tb_user, with data from users (simple user names such as U0001, U0002....).
I was doing the following:
CREATE TABLE tb_user_statshot (
user_code CHARACTER(5) NOT NULL
);
CREATE OR REPLACE FUNCTION sp_calculatet() RETURNS void AS
$$
DECLARE
BEGIN
INSERT INTO tb_user_statshot (user_code)
SELECT user_code FROM tb_user;
END;
$$ language 'plpgsql';
I expect the users code to reflect in my new statshot table, but this is no happening. Does anyone know why?
Thank you!

Create role programmatically with parameters

I'm looking for a way to create a function that takes in two parameters for user name and password and creates a read only role with it. I've tried something like:
create or replace function create_user_readonly (
unm varchar,
pwd varchar
)
returns varchar(10) as $$
begin
create role unm login password #pwd;
return 'success';
end;
$$ language plpgsql;
This throws the error:
[42601] ERROR: syntax error at or near "#" Position: 151
I thought of using dynamic SQL to construct the query but ran into this here (https://www.postgresql.org/docs/9.1/static/plpgsql-statements.html):
Another restriction on parameter symbols is that they only work in
SELECT, INSERT, UPDATE, and DELETE commands. In other statement types
(generically called utility statements), you must insert values textually
even if they are just data values.
here is an example:
create or replace function create_user_readonly (
unm varchar,
pwd varchar
)
returns varchar(10) as $$
begin
execute format($f$create role %I login password '%s'$f$,unm,pwd);
execute format('alter role %I set transaction_read_only to on',unm);
return 'success';
end;
$$ language plpgsql;
keep in mind though you will need to alter user to set transaction_read_only also to make it read only.
also:
CREATE ROLE does not offer setting RO default to role.
ALTER ROLE does
And keep in mind that overcoming uset configuration transaction_read_only is as easy as running one statement.
and create role won't give CONNECT permission, use CREATE USER instead if you want one.

Is it possible to get SQL-query by which a row was deleted?

I'm in PostgreSQL 9.4 and would like to write a trigger which could store sql-queries affects a secific row. What do I mean?
CREATE OR REPLACE FUNCTION partner.daily_profit_logger() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT'
INSERT INTO query_log (query) VALUES ( -- getting sql-query, performing the Insert operation
)
RETURN NEW;
END $$
language plpgsql;
Postgres has an extensive logging facility setup. You should probably look at log_statement = mod which will log all datamodifying queries (including but not restricted to INSERT and DELETE).
Cheers,

Postgresql regex in tsvector update

I have the following update trigger for a tsvector column
CREATE TRIGGER tsvector_user_update
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(user_tsv, 'pg_catalog.english', firstname, surname, email, card_id);
This works fine, however my card_id column (text) contains a pre-amble that the user is not aware of (it is added after the card is scanned), so I would like to strip out the pre-amble when the tsvector value is generated, I have tried the trigger function as a start
CREATE FUNCTION user_change_trigger() RETURNS trigger AS $$
BEGIN
NEW.user_tsv = setweight(to_tsvector('pg_catalog.english', coalesce(NEW.firstname,'')), 'A') ||
setweight(to_tsvector('pg_catalog.english', coalesce(NEW.surname,'')), 'A') ||
setweight(to_tsvector('pg_catalog.english', coalesce(REGEXP_REPLACE(NEW.card_id, '^\d+PRE', ''),'')), 'B') ||
setweight(to_tsvector('pg_catalog.english', coalesce(NEW.email,'')), 'C');
return new;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON users FOR EACH ROW EXECUTE PROCEDURE user_change_trigger();
Which executes, but I get the following:
WARNING: nonstandard use of escape in a string literal
And no updated tsvector
The pre-amble is an integer followed by 'PRE'.
(PostgreSQL 9.0)
Basic trigger design
The problem is of principal nature. In PostgreSQL you create a trigger function that does the work. I don't see your trigger function in the question.
Then you create a trigger which makes use of this function. You can only pass constants to a trigger function. Consider this quote from the manual about CREATE TRIGGER
function_name
A user-supplied function that is declared as taking no arguments and
returning type trigger, which is executed when the trigger fires.
arguments
An optional comma-separated list of arguments to be provided to the
function when the trigger is executed. The arguments are literal
string constants. Simple names and numeric constants can be written
here, too, but they will all be converted to strings. Please check the
description of the implementation language of the trigger function to
find out how these arguments can be accessed within the function; it
might be different from normal function arguments.
Bold emphasis mine.
Use NEW to access the column values inside the trigger function. You don't need to pass them as arguments. Get a grip on the basic concept first. Start here.
regexp_replace()
Use:
regexp_replace(card_id, '^\d+PRE', '')
.. since the leading characters are supposed to be digits only (and at least one of them).
Proper trigger & function
The following test case works for me on PostgreSQL 9.1.6. Your version looks basically good to me, I only made minor changes. But keep reading ...
Create test environment (will be rolled back at the end):
BEGIN;
CREATE SCHEMA test;
SET search_path = test;
CREATE TABLE users (
users_id serial primary key
,firstname text
,surname text
,card_id text
,email text
,user_tsv tsvector
);
Trigger function:
CREATE FUNCTION user_change_trigger()
RETURNS trigger AS
$func$
BEGIN
NEW.user_tsv :=
setweight(to_tsvector('pg_catalog.english', coalesce(NEW.firstname,'')), 'A')
|| setweight(to_tsvector('pg_catalog.english', coalesce(NEW.surname,'')), 'A')
|| setweight(to_tsvector('pg_catalog.english', coalesce(regexp_replace(NEW.card_id, '^\d+PRE', ''),'')), 'B')
|| setweight(to_tsvector('pg_catalog.english', coalesce(NEW.email,'')), 'C');
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
The assignment operator of plpgsql is := - unlike SQL where = is used.
Trigger:
CREATE TRIGGER tsvectorupdate
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE PROCEDURE user_change_trigger();
Tests:
INSERT INTO users (firstname, surname, card_id, email)
VALUES ('Erwin', 'Brandstetter', '123PRE456', 'foo#dummy.org')
RETURNING *;
-- looks good!
UPDATE users SET firstname = 'Walter' WHERE TRUE
RETURNING *;
-- looks good, too!
Clean up:
ROLLBACK;
standard_conforming_strings
Explore your setting of standard_conforming_strings. The WARNING suggests that you don't have this setting on, which would require that you double the backslash in:
'^\\d+PRE'

Postgres: checking value before conditionally running an update or delete

I've got a fairly simple table which stores the records' authors in a text field as shown here:
CREATE TABLE "public"."test_tbl" (
"index" SERIAL,
"testdate" DATE,
"pfr_author" TEXT DEFAULT "current_user"(),
CONSTRAINT "test_tbl_pkey" PRIMARY KEY("index");
The user will never see the index or pfr_author fields, but I'd like them to be able to UPDATE the testdate field or DELETE whole records if they have permission and if they are the author. i.e. if test_tbl.pfr_author = CURRENT_USER THEN permit the UPDATE OR DELETE, but if not then raise an error message such as "Sorry, you do not have permission to edit this record.".
I have not gone down the route of using a trigger as I figure that even if it is executed before row update the user-requested update will still take place afterwards regardless.
I've tried doing this through a rule, but end up with infinite recursion as I put an update command inside the rule. Is there some way to do this using rules alone or a combination of a rule and trigger?
Thanks very much for any help!
Use a row level BEFORE trigger on UPDATE and DELETE to do this. Just have it return NULL when the operation is not permitted and the operation will be skipped.
http://www.postgresql.org/docs/9.0/interactive/trigger-definition.html
the trigger function have some problem,resulting recursive loop update.You should do like this:
CREATE OR REPLACE FUNCTION "public"."test_tbl_trig_func" () RETURNS trigger AS $body$
BEGIN
IF not (old.pfr_author = "current_user"() OR "current_user"() = 'postgres') THEN
NULL;
END IF;
RETURN new;
END;
$body$ LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER COST 100;
I have a test like this,it does well;
UPDATE test_tbl SET testdate = CURRENT_DATE WHERE test_tbl."index" = 2;