0

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();
Firat Oezcan
  • 60
  • 1
  • 4

1 Answers1

1

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();
Laurenz Albe
  • 209,280
  • 17
  • 206
  • 263
  • A guest is any invoker of my API that doesn't have a role or a user_id set in their authorization headers. I don't see a way to modify my policies to allow an INSERT followed up by returning exactly the row inserted right now AND restrict access to only the rows the user owns. The `SECURITY DEFINER` seems like the way I did it before: Only allowing `INSERT`s through my own function which is, I guess, ok. But not the gist of "updating the security context within a trigger so the followup `RETURNING *` works – Firat Oezcan Feb 20 '20 at 09:05
  • 1
    A advise against your idea with a trigger. I guess I must me missing something, but I don't see why you don't define a permissive `INSERT` policy for guests. – Laurenz Albe Feb 20 '20 at 09:28
  • It's not the `INSERT` as an issue here but the `RETURNING *` afterwards which makes me to have some kind of `SELECT` policy for guest users. The `INSERT` goes through completely fine without any kind of triggers but when doing a `INSERT INTO ... RETURNING *` it doesn't work. I cannot not do it a different way because that is the way Postgraphile (the API layer I use ontop) does it and migrating away from it because of that is too much of an architectural cost. – Firat Oezcan Feb 20 '20 at 11:29
  • 1
    I have extended the answer to give you a hint for the trigger function. – Laurenz Albe Feb 20 '20 at 11:48
  • Oh wow, I was missing the type on the `NEW.id`. That makes it clearer that I need to provide that. Thanks for the help, that was the missing piece in my syntax. (Apart from using the `BEFORE` trigger) – Firat Oezcan Feb 20 '20 at 12:40