Unable to update row of table with Row Level Security - postgresql

I'm trying to update a row in a table with row level security policies, but keep getting the error new row violates row-level security policy for table "my_table".
Here's how I set up RLS policies:
alter table my_table enable row level security;
alter table my_table force row level security;
create policy select_policy on my_table for select to public using (deleted is false);
create policy insert_policy on my_table for insert to public with check (true);
create policy delete_policy on my_table for delete to public using (true);
create policy update_policy on my_table for update to public using (true) with check (true);
The query I'm trying to run is:
update my_table set deleted = true where id = 1;
I need to perform a "soft-delete" of the rows in my_table in this way - by switching the deleted flag.
What am I doing wrong here? How do I make such queries work?
Update #1
Steps to Reproduce:
create table if not exists my_table (
"name" varchar(40),
deleted boolean default false
);
insert into my_table (name) values ('John'), ('Alice'), ('Bob');
alter table my_table enable row level security;
alter table my_table force row level security;
drop policy if exists my_table_select_policy on my_table;
drop policy if exists my_table_insert_policy on my_table;
drop policy if exists my_table_delete_policy on my_table;
drop policy if exists my_table_update_policy on my_table;
create policy my_table_select_policy on my_table for select to public using (deleted is false);
create policy my_table_insert_policy on my_table for insert to public with check (true);
create policy my_table_delete_policy on my_table for delete to public using (true);
create policy my_table_update_policy on my_table for update to public using (true);
update my_table set deleted = true where name = 'John'; -- throws error
On the screenshot below are the privileges of current_user:
My current user's grant is grant all on schema public to my_user;

Postgres applies the my_table_select_policy on the updated row (having deleted = false). For a reason unknown to me.
As a workaround I would suggest to build in a grace period where the my_table_select_policy still returns true:
instead of delete have a deleted_at column and on deletion store the current timestamp in it (i.e. timestamp when the deletion happened)
in the SELECT policy check if deleted_at is NULL or the time between deleted_at and now is less than 1 second
USING (
my_table.deleted_at IS NULL
OR
ABS(EXTRACT(EPOCH FROM (now() - my_table.deleted_at))) < 1
)

Related

bigint id changed back to int during table rename

I hit the int limit on a large table I use.
The table is in single user mode and has no FK constraints.
CREATE TABLE my_table_bigint (LIKE my_table INCLUDING ALL);
ALTER TABLE my_table_bigint ALTER id DROP DEFAULT;
ALTER TABLE my_table_bigint alter column id set data type bigint;
CREATE SEQUENCE my_table_bigint_id_seq;
INSERT INTO my_table_bigint SELECT * FROM my_table;
ALTER TABLE my_table_bigint ALTER id SET DEFAULT nextval('my_table_bigint_id_seq');
ALTER SEQUENCE my_table_bigint_id_seq OWNED BY my_table_bigint.id;
SELECT setval('my_table_bigint_id_seq', (SELECT max(id) FROM my_table_bigint), true);
At this point I tested that I could insert new rows without any problems. Success, I thought.
I went about renaming the tables.
alter table my_table rename my_table_old
alter table my_table_bigint rename my_table
ALTER INDEX post_comments_pkey RENAME TO post_comments_old_pkey
ALTER INDEX post_comments_pkey_bigint RENAME TO post_comments_pkey
Now, when I checked the schema.... the table ID type had changed BACK to integer, instead of bigint.
Copying took about 3 days - so I am really, really hoping that I don't need to do this again. This is postgres10 on RDS.
EDIT
I'm going to take care of this problem like this:
Create a new table - call it my_table_bigint2.
Do this:
CREATE TABLE my_table_bigint2 (LIKE my_table INCLUDING ALL);
ALTER TABLE my_table_bigint2 ALTER id DROP DEFAULT;
ALTER TABLE my_table_bigint2 alter column id set data type bigint;
CREATE SEQUENCE my_table_bigint2_id_seq;
ALTER TABLE my_table_bigint2 ALTER id SET DEFAULT nextval('my_table_bigint2_id_seq');
ALTER SEQUENCE my_table_bigint2_id_seq OWNED BY my_table_bigint2.id;
And start populating that table with the new data. (This is fine given the usecase.)
In the meantime, I'm going to run
ALTER TABLE post_comments alter column id set data type bigint;
And finally, once that's done, I'm going to
INSERT INTO my_table SELECT * FROM my_table_bigint2;
My follow-up question - is this allowed? Will this create some interaction between the sequences? Should I use a new sequence?

Postgres row level policy issue

I am trying out the Postgres row level security feature and not being able to see it working. Not sure what I am missing.
CREATE TABLE tenants (id uuid PRIMARY KEY, name TEXT);
INSERT INTO tenants (id, name) values ('ec5e9a6b-ed71-4e41-bc1e-11dac6808e41', 'Tenant1'), ('a684edc2-19b2-40d6-b679-519a6f736981', 'Tenant2');
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY ;
ALTER TABLE tenants FORCE ROW LEVEL SECURITY;
SET app.tenant_id = 'ec5e9a6b-ed71-4e41-bc1e-11dac6808e41';
CREATE POLICY tenants_policy ON tenants FOR ALL USING ( current_setting('app.tenant_id')::uuid = id );
SELECT * FROM tenants;
For the last select, I expected it to return only one row with id 'ec5e9a6b-ed71-4e41-bc1e-11dac6808e41' but it is returning both rows. What am I missing? Thank you!
Your example works for me. There are a few possibilities:
The current user is a superuser.
The current user is defined with BYPASSRLS.
The configuration parameter row_security is off.

How Do I Insert Into a Table With Row Level Security?

I have the following table:
Table "api_v1.person"
Column | Type | Modifiers
---------------+--------+-------------------------------------------------------
person_id | bigint | not null default...
name | text | not null
date_of_birth | date |
api_user | text | not null default "current_user"()
That has the following policy:
POLICY "api_user_only" FOR ALL
USING ((api_user = ("current_user"())::text))
WITH CHECK ((api_user = ("current_user"())::text))
My understanding is that the FOR ALL portion of the policy means that it covers inserts and the WITH CHECK ensures that the value inserted into api_user is the same as the current user, eg the role name. The USING clause should only effect SELECTS or other data that is returned. However, when I try to insert I get the following results:
demo=> INSERT INTO api_v1.person (name, api_user) VALUES ('Greg', current_user);
ERROR: query would be affected by row-level security policy for table "person"
How do I do this insert?
I'm running PostgreSQL 9.6.8.
Here is the SQL necessary to reproduce:
BEGIN;
CREATE SCHEMA api_v1;
CREATE TABLE api_v1.person (
person_id BIGSERIAL PRIMARY KEY,
"name" TEXT NOT NULL,
date_of_birth DATE,
api_user TEXT NOT NULL DEFAULT current_user
);
ALTER TABLE api_v1.person ENABLE ROW LEVEL SECURITY;
CREATE POLICY
api_user_only
ON
api_v1.person
USING
(api_user = CURRENT_USER)
WITH CHECK
(api_user = CURRENT_USER)
;
CREATE ROLE test_role;
GRANT USAGE ON SCHEMA api_v1 TO test_role;
GRANT ALL ON api_v1.person TO test_role;
GRANT USAGE ON SEQUENCE api_v1.person_person_id_seq TO test_role;
COMMIT;
SET ROLE test_role;
INSERT INTO api_v1.person ("name") VALUES ('Greg');
There is a setting in postgresql.conf, row_security. If this is set to off then any query that would be effected by a row level security policy fails with the error: ERROR: query would be affected by row-level security policy for table "table_name". However, queries from superusers, the table owner (if you don't force RLS), and roles with bypassrls will work.
The row_security setting needs to be set to on and then PostgreSQL needs to be restarted for regular user statements to be processed against tables with row level security policies.
From the source code:
/*
* We should apply RLS. However, the user may turn off the row_security
* GUC to get a forced error instead.
*/
if (!row_security && !noError)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("query would be affected by row-level security policy for table \"%s\"",
get_rel_name(relid)),
amowner ? errhint("To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL SECURITY.") : 0));

Prevent updates to generated primary key column

In PostgreSQL I have created a table and with an id column defined as serial. I have inserted two rows, but I can still update the value of the id column.
But I need prevent updates to the generated value of the id column.
create table aas.apa_testtable
(
id serial primary key,
name text
)
insert into aas.apa_testtable(name) select ('test')
insert into aas.apa_testtable(name) select ('test2')
-- I want this to be impossible / result in an error:
update aas.apa_testtable set id=3 where id=2
You can revoke update on table and grant it on column(s):
REVOKE UPDATE ON TABLE aas.apa_testtable FROM some_role;
GRANT UPDATE (name) ON TABLE aas.apa_testtable TO some_role;
Remember about role public, superusers and other inheritance issues you might have in your setup.
--Do not try this, it will not work without revoking table level privileges:
REVOKE UPDATE (id) ON TABLE aas.apa_testtable FROM some_role;
Alternative is to create trigger that will check if old != new, but with details provided I don't see need for it.

Alter column set not null fails

Consider the following table with approximately 10M rows
CREATE TABLE user
(
id bigint NOT NULL,
...
CONSTRAINT user_pk PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
)
Then i applied the following alter
ALTER TABLE USER ADD COLUMN BUSINESS_ID VARCHAR2(50);
--OK
UPDATE USER SET BUSINESS_ID = ID; //~1500 sec
--OK
ALTER TABLE USER ALTER COLUMN BUSINESS_ID SET NOT NULL;
ERROR: column "business_id" contains null values
SQL state: 23502
This is very strange since id column (which has been copied to business_id column) can't contain null values since it is the primary key, but to be sure i check it
select count(*) from USER where BUSINESS_ID is null
--0 records
I suspect that this is a bug, just wondering if i am missing something trivial
The only logical explanation would be a concurrent INSERT.
(Using tbl instead of the reserved word user as table name.)
ALTER TABLE tbl ADD COLUMN BUSINESS_ID VARCHAR2(50);
--OK
UPDATE tbl SET BUSINESS_ID = ID; //~1500 sec
--OK
-- concurrent INSERT HERE !!!
ALTER TABLE tbl ALTER COLUMN BUSINESS_ID SET NOT NULL;</code></pre>
To prevent this, use instead:
ALTER TABLE tbl
ADD COLUMN BUSINESS_ID VARCHAR(50) DEFAULT ''; -- or whatever is appropriate
...
You may end up with a default value in some rows. You might want to check.
Or run everything as transaction block:
BEGIN;
-- LOCK tbl; -- not needed
ALTER ...
UPDATE ...
ALTER ...
COMMIT;
You might take an exclusive lock to be sure, but ALTER TABLE .. ADD COLUMN takes an ACCESS EXCLUSIVE lock anyway. (Which is only released at the end of the transaction, like all locks.)
Maybe it wants a default value? Postgresql docs on ALTER:
To add a column, use a command like this:
ALTER TABLE products ADD COLUMN description text;
The new column is initially filled with whatever default value is given (null if you don't specify a DEFAULT clause).
So,
ALTER TABLE USER ALTER COLUMN BUSINESS_ID SET DEFAULT="",
ALTER COLUMN BUSINESS_ID SET NOT NULL;
You cannot do that at the same transaction. Add your column and update it. Then in a separate transaction set the not null constraint.