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

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));

Related

Unable to update row of table with Row Level Security

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
)

Using PostgreSQL row level security (RLS) policies with current_setting() function

I've applied RLS policy to the "users" table and expect only records with tenant_id=2 to be retrieve:
CREATE TABLE "users" ("name" text UNIQUE NOT NULL, "tenant_id" int NOT NULL DEFAULT current_setting('app.current_tenant')::int);
--Enable Row Security Policies
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE users FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON users USING (tenant_id = current_setting('app.current_tenant')::int);
--Set "111" as the current tenant.
SET app.current_tenant TO 1;
INSERT INTO users VALUES ('admin');
INSERT INTO users VALUES ('bob');
--Set "222" as the current tenant.
SET app.current_tenant TO 2;
INSERT INTO users VALUES ('alice');
--Data output
SELECT * FROM users;
But I get all users in the result:
name tenant_id
admin 1
bob 1
alice 2
Why is this happening?
Here is the dbFiddle of what I am stuck with:
https://www.db-fiddle.com/f/iFktvVsDNYKggUNT2oDJBV/0
There are four reasons why row level security can be bypassed:
The user is the owner of the table.
You can subject the table owner to row level security with
ALTER TABLE users FORCE ROW LEVEL SECURITY;
The user is a superuser.
The user was created with BYPASSRLS.
The database parameter row_security is set to off.
Note that using row level security with a placeholder parameter is inherently insecure: if an attacker can issue an SQL statement (say, through SQL injection), they can just change the value to what they like.

PostgreSQL RLS policy to specific user is not working as expected and applies to all users

I have 2 users in my DB, one is "strong" and one is "weak".
I want to apply RLS policy only for one of them, the weak user.
Meaning, when strong user queries the table, it should get all rows. But when weak user queries the table, the policy will be applied and it will return only allowed rows.
I have created a table, and applied the RLS policy only to the weak user.
But even when querying with the strong user, the policy is executed and prevents me from getting all rows.
I'm using PostgreSQL version 11.4.
Here is how I created the policy (I've created the policy with another 3rd user which is an admin and the owner of the table)
CREATE TABLE account_test
(
id bigserial not null,
description varchar(200),
tenant_id UUID not null
);
ALTER TABLE account_test ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_policy ON account_test TO weak_user
USING (tenant_id = current_setting('rls.tenant_id')::uuid);
account=# select * from pg_policies;
schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check
------------+--------------+---------------+------------+---------------+-----+--------------------------------------------------------------+------------
account | account_test | tenant_policy | PERMISSIVE | **{weak_user}** | ALL | (tenant_id = (current_setting('rls.tenant_id'::text))::uuid) |
Now, inserting and selecting with admin user always works, because it is the owner:
insert into account_test (description, tenant_id) VALUES ('desc111', '11111111-c929-462e-ade4-074c81643191');
select * from account_test;
no problem here and all rows returned.
When trying to login with weak_user and select, I get no rows as expected:
select * from account_test;
-- returns 0 rows as expected (weak_user).
If I set the parameter, policy applies and I get the data as expected:
select set_config('rls.tenant_id', '11111111-c929-462e-ade4-074c81643191',true);
select * from account_test;
-- returns 1 row as expected
Now, when I login with strong_user and perform the select * from account_test query, I expect all rows to be returned because policy applies only for weak_user.
However, I get the same behavior as for weak_user and no rows return.
Also the query with set_config does not return anything.
What am I missing?
Is that the expected behavior?
Can someone explain?
The moment you enable RLS on a table with ALTER TABLE account_test ENABLE ROW LEVEL SECURITY, a default-deny all policy is used for all users.
When [RLS] is enabled on a table, all normal access to the table for selecting rows or modifying rows must be allowed by a row security policy. (However, the table's owner is typically not subject to row security policies.) If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified.
– https://www.postgresql.org/docs/current/ddl-rowsecurity.html
Each policy has a name and multiple policies can be defined for a table. As policies are table-specific, each policy for a table must have a unique name. Different tables may have policies with the same name.
In order for strong_user to have access, you will need to add another rule, e.g.
CREATE POLICY tenant_policy ON account_test TO strong_user USING (true);
When multiple policies apply to a given query, they are combined using either OR (for permissive policies, which are the default) or using AND (for restrictive policies). This is similar to the rule that a given role has the privileges of all roles that they are a member of. Permissive vs. restrictive policies are discussed further below.

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.

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.