question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Insert causes select policy violation due to select occuring before all insert triggers are finished

See original GitHub issue

Bug report

Description of issue

Supabase (Or PostgREST) is performing a select operation during row insertion, which throws a select policy violation. However the conditions of the select policy should be valid if the select operation waited until all triggers on the insert were completed. Here is a more in depth explanation of what my setup is:

The setup

I have tables page and user where a user has access to a page via a join table page_user.

create table page (
  id uuid primary key
);

create table user (
  id uuid primary key
);

create table page_user (
  page_id uuid references page,
  user_id uuid references user,
  primary key (page_id, user_id)
);

I have an RLS policy that restrict select on a page to any user with a matching page_user entry.

create policy "page select" on page
  for select
  using (
   exists (
     select 1 from page_user where user_id = auth.uid() and page_id = id
    )
  );

create policy "page insert" on page
  for insert
  with check (
    true
  );

I have a trigger on page after insert that automatically inserts a page_user entry for the page and user that is creating the page.

create function page_after_insert()
  returns trigger as
  $$
    begin      
      insert into page_user (page_id, user_id) values (NEW.id, auth.uid());
      return NEW;
    end;
  $$ language plpgsql security definer;

create trigger page_after_insert after insert on page
  for each row execute procedure page_after_insert();

The problem

When I use Supabase (via PostgREST) to insert a new page I get a violation on the “page select” policy. This is because PostgREST uses a select to send the new page id to the client after a successful insert, which I want. However it seems like this select query is being run before the “page_after_insert” trigger is run so the page_user entry doesn’t exist yet. I can fix this by running the trigger before insert with a deferred reference constraint on the page_id column, but this seems like a hack. Ideally the full insert with triggers and transactions would be fulfilled before postgrest performs the select.

Does this make sense? Is this an expected behavior? I always thought that triggers are performed as part of a transaction so I could expect the page_user insert to happen before a subsequent select.

Hope someone can look into this. Thank you for taking a look when you get a chance!

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:1
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
GaryAustin1commented, Jun 16, 2022

@steve-chavez So just to clarify for my understanding. There is no actual select statement. The returning is part of the insert, which has to meet select policy during the insert operation. An after trigger occurs after both the insert of data to the db and the returning part of the operation.

1reaction
steve-chavezcommented, Jun 16, 2022

Supabase (Or PostgREST) is performing a select operation during row insertion https://nikofischer.com/supabase-error-row-level-security-policy

Have you tried using returning=minimal?

await supabase.from('sample').insert({ key: 'value' }, { returning: 'minimal' })

I’m not quite sure what the order of operations is behind the scenes.

Here’s a sample generated query. Using the below:

create table page (
  id int primary key,
  name text
);

const { data } = await supabase.from('page').insert({ "id": 1, "name": "page-1" });

This is generated:

-- replace $1 by '{ "id": 1, "name": "page-1" }'

WITH 
pgrst_payload AS (SELECT $1::json AS json_data), 
pgrst_body AS ( SELECT CASE WHEN json_typeof(json_data) = 'array' THEN json_data ELSE json_build_array(json_data) END AS val FROM pgrst_payload) 
INSERT INTO "test"."page"("id", "name") SELECT "id", "name" FROM json_populate_recordset (null::"test"."page", (SELECT val FROM pgrst_body)) _  
RETURNING "test"."page".*

When using returning=minimal, the last clause changes to RETURNING 1.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Postgres Trigger side-effect is occurring out of order with row ...
The SELECT 's USING policy select * from b where a.id = b.id is evaluated. This should return true due to step 3;...
Read more >
SQL Server triggers: The good and the scary - Simple Talk
A trigger is a set of code that is evaluated when a data change is made to a table. Triggers may be defined...
Read more >
Documentation: 15: CREATE TRIGGER - PostgreSQL
The trigger can be specified to fire before the operation is attempted on a row (before constraints are checked and the INSERT ,...
Read more >
Database Engine events and errors - SQL Server
For a full list of all errors, query the sys.messages catalog view ... The number of SELECT values must match the number of...
Read more >
Common Issues with the SQL Server Import and Export Wizard
In my case I've just selected all the tables in my database : ... If you select the 'Enable identity insert' as indicated...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found