RLS (Row Level Security)
The main idea behind RLS is that we should be able grant access to a specific set of rows of a table, but not to the whole table itself.
- A result of this is that our application logic only has to worry about
SELECT * FROM my_tableand RLS will handle theWHERE user_id = my_user_idpart automagically. - To put it another way: our queries should only contain the clauses requested by our interfaces and not the filters and conditions demanded by access control in a multi-tenant data store.
- The current PG role that is accessing the table must have been
granted permission to use it. Otherwise, RLS errors will arise when we try to alter something in the table as that role, because we won't be able to access the table from the outset.
A function marked with SECURITY DEFINER will bypass RLS.
If a user has been GRANTed access to a given table, then they will be able to bypass any RLS restrictions.
Policies are created using the CREATE POLICY command
- RLS is opt-in (ie. disabled by default)
- When RLS is enabled, all rows are by default not visible to any roles (superusers still have access).
- If the value in parentheses after USING evaluates to true, then the user gets permission
- ex. imagine we have a chat app, and we want to ensure a user can only see messages sent by him, and messages intended for him. Also, we want to ensure that users cannot modify the
message_fromcolumn to make it seem that the message is coming from someone else:
CREATE TABLE chat (
message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
message_time TIMESTAMP NOT NULL DEFAULT now(),
message_from NAME NOT NULL DEFAULT current_user,
message_to NAME NOT NULL,
message_subject VARCHAR(64) NOT NULL,
message_body TEXT
);
CREATE POLICY chat_policy ON chat
USING ((message_to = current_user)
OR (message_from = current_user))
WITH CHECK (message_from = current_user)
- RLS can be implemented using jwt claims to verify that the user is who they say they are, if we do not want to use
current_user:- the second arg true means "return null if the setting is missing"
CREATE POLICY chat_policy ON chat
USING ((message_to = current_setting('request.jwt.claim.email', true))
OR (message_from = current_setting('request.jwt.claim.email', true)))
WITH CHECK (message_from = current_setting('request.jwt.claim.email', true))
- When request contains a valid JWT with a role claim (
jwt.claims.role), we should switch to the role with that name for the duration of the HTTP request
RLS Policy using external tables
- What if we want to enable RLS where
user_id = current_user_id(), but the current table does not keep auser_idcolumn? - If we are adding an RLS policy to T1, but the policy depends on a
JOINable table T2, then T2 must havegranted privileges to the PG role accessing the table.
create policy t2_policy_update on t2 for update using (
exists (
select 1
from t1
inner join t0
on t1.t0id = t0.id
where t0.u = session_user
and t1.id = t1.id
)
)
Subqueries in RLS policies respect the RLS policies of the tables they reference
Per-command Policies
UPDATE
- Since
UPDATEinvolves pulling an existing record and replacing it with a new modified record,UPDATEpolicies accept both aUSINGexpression and aWITH CHECKexpressionUSINGdetermines which records theUPDATEcommand will see to operate againstWITH CHECKdefines which modified rows are allowed to be stored back into the table.- If the updated value fails the
WITH CHECKexpression, there will be an error. - If only a
USINGclause is specified, then it will be used for bothUSINGandWITH CHECKcases (ie.WITH CHECKis implemented for us implicitly)
- If the updated value fails the
- Typically an
UPDATEcommand needs to read data from columns in the relation being updated (e.g. in aWHEREclause, orRETURNINGclause, or right side of aSETclause).- In cases such as these,
SELECTrights are required on the relation being updated, in addition to theUPDATEright.
- In cases such as these,
Anatomy
WITH CHECK vs USING
- USING is applied before any operation occurs to the table’s rows
- ex. in the case of updating a nugget, one could not update a row that does not have the appropriate user_id in the first place
- must use USING with DELETE commands because a delete changes no rows, and only removes current ones.
- USING implicitly runs a WITH CHECK with the same clause that USING received, meaning that the verification operation runs both before and after the data is inserted.
- WITH CHECK is run after an operation is applied, so if it fails, the operation will be rejected
- ex. in the case of an insert, Postgres sets all of the columns as specified and then compares against WITH CHECK on the new row
- must use WITH CHECK with INSERT commands because there are no rows to compare against before insertion
Permissive or Restrictive
RLS policies can be either permissive or restrictive
- permissive (default) - in consideration of all RLS policies, only 1 must pass
- restrictive - in consideration of all RLS policies, all must
create policy select_all on table_name as permissive using (true)
Infinite Recursion
Imagine we are making a RLS policy for select on a given table. If we then try and select that same table within the using() function, we will get an infinite recursion as a result.
Check if RLS enabled
select relname, relrowsecurity, relforcerowsecurity
from pg_class
where oid = 'your_table_name_with_schema'::regclass;
or
select * from pg_tables where tablename = 'your_table_name_without_schema'