I know whats the problem is (below) and have a workaround, but i would like to get rid of it.
I would like to implement soft delete for my_table.
The Problem is that if i run UPDATE "my_table" set delete_at = now() where id = ...
i get new row violates row-level security policy for table
.
The insert and delete (real delete) is working fine, so i skip this here.
-- create table
CREATE TABLE "my_table" (
"id" SERIAL PRIMARY KEY
"user_id" int not null references "user"(id) on delete cascade,
deleted_at timestamp with time zone
);
-- enable row level security
ALTER TABLE "my_table" ENABLE ROW LEVEL SECURITY;
-- allow read own data and everything for admin
CREATE POLICY my_table_read_policy ON "my_table" FOR SELECT
USING (
current_setting('role') = 'app_admin'
or
(
current_setting('role') = 'app_user'
and user_id = get_current_user_id()
and (
deleted_at is null
)
)
);
-- update, e.g. set delete_at date
CREATE POLICY my_table_update_policy ON "my_table" FOR UPDATE
USING (true)
WITH CHECK (
current_setting('role') = 'app_admin'
or
(
current_setting('role') = 'app_user'
and user_id = get_current_user_id()
)
);
I found that this happens because of my SELECT Policy which filters by deleted_at.
After changing the SELECT Policy to the following workaround (delete_at is null OR "within the last second")
, it works.
-- allow read own data and everything for admin
CREATE POLICY my_table_read_policy ON "my_table" FOR SELECT
USING (
current_setting('role') = 'app_admin'
or
(
current_setting('role') = 'app_user'
and user_id = get_current_user_id()
-- workaround: https://stackoverflow.com/a/74306008
and (
deleted_at is null OR ABS(EXTRACT(EPOCH FROM (now() - deleted_at))) < 1
)
)
);
2
Answers
Why is this happening?
As per
Table 287
(Policies Applied by Command Type), when you execute anUPDATE
command, if you haveWHERE
orRETURNING
in your query thenSELECT
policies are also applied to both existing and new rows. With RLS, you have yourSELECT
/USING
policies evaluate toTRUE
for any conditionalUPDATE
to happen.In simpler terms, in order to update a specific
user_id
, you have to be able to read the row with thatuser_id
. Otherwise, it would be 0 rows to update.Your update query features
where user_id =
. More than that, policies are considered part of the query as well. So, as long as you have a reference to a field within yourCHECK
policies,UPDATE
queries will always triggerSELECT
/USING
policies.How to get rid of this workaround?
Semantically speaking, implementing soft delete introduces another logical layer that splits row state into two deleted states. This is the root for this messy interaction, and the best way of dealing with it would be to not use soft delete fields within RLS.
However, if you still want to do this, and don’t like having that
< 1s
condition in your policies — which makes sense, those would fail for updates that are taking longer than 1s to run — you can still achieve what you want, yet have somewhat clean policies.The solution would be to create a specific soft delete function, that will switch current
role
toapp_admin
, soft delete the record, and then set role back toapp_user
. The function will have to check that targetuser_id
matches currentuser_id
, of course.Something along these lines will do the trick, although it is highly not recommended
Got the same issue recently and read multiple articles promoting an "archive table" strategy that works better anyway, for the following reasons:
insert into active_table select * from archive_table where id = ?
than juggling with RLS and rolesfor each statement
trigger and their transition tableAll in all, the effort you have to provide with a
deleted_at
column might be higher than with an archive table.