Skip RLS checks temporarily - postgresql

I have tables with enabled row-level-security and the relevant policies in place - working really well.
My problem is that sometimes, based on some conditions, I want to bypass a policy for a specific statement during a function execution.
something like:
...
statement 1
statement 2
if (some cond) then
disable rls temporarily
statement 3 -- mostly delete rows the user can't normally see
enable rls
else
statement 3
end if
The way I've implemented it is by creating a function check_cond which returns a boolean evaluating some cond and created an additional select policy which calls this check_cond.
It works - but the actual problem is that the query select * from tab now looks like this:
select * from tab where <original policy condition> or check_cond()
This or check_cond() causes postges to always do a full table scan since it cannot evaluate the result pre-planning.
If I were able to write "dynamic" code in the policy I would have been able to add/remove conditions based on the value of check_cond() but as far as I know it's not possible.
Any smart way which would allow me to temporarily disable rls or dynamically add conditions without sacrificing performance?
Thanks.

The easiest way would be to have a SECURITY DEFINER function owned by a superuser which runs:
ALTER ROLE someuser BYPASSRLS;
where someuser is the user that runs the SQL statements.
Afterwards, you can re-enable it in the same way.
But that's pretty insecure, because nothing keeps the user from calling theses functions at other times.
A better way would be to define a security definer function owned by a user with BYPASSRLS that does the deletes for you.
Note: for security reasons, always SET search_path when you define a SECURITY DEFINER function.

Related

Possible to restrict PostgreSQL security definer function to RLS use?

I am using RLS (Row Level Security) with supabase.io for a "serverless" application. I have to use various security definer functions for RLS policies. These are still callable through supabase's rpc library. Is there anyway to limit calling these functions to either the admin (me) or when used as part of a RLS policy?
e.g.:
CREATE OR REPLACE FUNCTION get_bases_editable_or_viewable_for_user(user_id uuid, allow_edit bool)
returns setof bigint as $$
select base_id
from access_controls
where access_controls.user_id = $1 AND ($2 AND access_controls.access_level = 'editor') OR access_controls.access_level = 'viewer';
$$ stable language sql security definer;
CREATE policy "Users can read bases they are editors or viewers of"
on public.bases
for select using ( bases.id in (get_bases_editable_or_viewable_for_user(auth.uid(), true)) );
get_bases_editable_or_viewable_for_user allows any user, once they have another user's UID, to find out the UIDs that this user has access to as an editor or viewer:
supabase.rpc(
"get_bases_editable_or_viewable_for_user",
{ user_id: "dddddde6-1111-4bdf-aaaa-33336ccc31ee", allow_edit: true }
)
.then(console.log) // => bad
Minimising opportunities for leaking information is always important for maximising the security of an application and the privacy of its users.
You cannot restrict permissions on the function in that way, since the user that runs the query must be able to execute it.
I see two ways to improve that:
Omit the first parameter from the function, so that it only gives results for the current user. Then nobody can see information for other users.
In addition to the above, you could pass bases.id as a function parameter and have the function return a boolean. Then you cannot get a list, but the performance may suffer, since the function has to be called for each row.

Postgresql row level security does not throw errors

I have a Postgresql DB that I want to enable the Row-Level-Security on one of its tables.
Everything is working fine, except one thing, that is I want to have an error to be thrown when a user try to perform an update on a record that he doesn't have privileges on.
According to the docs:
check_expression: Any SQL conditional expression (returning boolean).
The conditional expression cannot contain any aggregate or window
functions. This expression will be used in INSERT and UPDATE queries
against the table if row level security is enabled. Only rows for
which the expression evaluates to true will be allowed. An error will
be thrown if the expression evaluates to false or null for any of the
records inserted or any of the records that result from the update.
Note that the check_expression is evaluated against the proposed new
contents of the row, not the original contents.
So I tried the following:
CREATE POLICY update_policy ON my_table FOR UPDATE TO editors
USING (has_edit_privilege(user_name))
WITH CHECK (has_edit_privilege(user_name));
I have also another policy for SELECT
CREATE POLICY select_policy ON my_table FOR SELECT TO editors
USING (has_select_privilege(user_name));
According to the docs, this should create a policy that would prevent any one from the editors ROLE to perform update on any record of my_table, and would throw an error when an update is performed. This works correctly, but no error is thrown.
What's my problem?
Please help.
First, let me explain how row level security works when reading from the table:
You need not even have to define the policy – if there is no policy for a user on a table with row level security, the default is that the user can see nothing.
No error will be thrown when reading from a table.
If you want an error to be thrown, you could write a function
CREATE FUNCTION test_row(my_table) RETURNS boolean
LANGUAGE plpgsql COST 10000 AS
$$BEGIN
IF /* the user is not allowed */
THEN
RAISE EXCEPTION ...;
END IF;
RETURN TRUE;
$$END;$$;
Then use that function in your policy:
CREATE POLICY update_policy ON my_table FOR UPDATE TO editors
USING (test_row(my_table));
I used COST 10000 for the function to tell PostgreSQL to test that condition after all other conditions, if possible.
This is not a fool-proof technique, but it will work for simple queries. What could happen in the general case is that some conditions get checked after the condition from the policy, and this could lead to errors with rows that wouldn't even be returned from the query.
But I think it is the best you can get when abusing the concept of row level security.
Now let me explain about writing to the table:
Any attempt to write a row to the table that does not satisfy the CHECK clause will cause an error as documented.
Now let's put it together:
Assuming that you define the policy like in your question:
Any INSERT will cause an error.
Any SELECT will return an empty result.
Any UPDATE or DELETE will be successful, but affect no row. This is because these operations have to read (scan) the table before they modify data, and that scan will return no rows like in the SELECT case. Since no rows are affected by the data modification, no error is thrown.

What does this select statement actually do?

I'm reviewing log of executed PostgreSQL statements and stumble upon one statement I can't totally understand. Can somebody explain what PostgreSQL actually do when such query is executed? What is siq_query?
select *
from siq_query('', '21:1', '', '("my search string")', False, True, 'http://siqfindex:8080/storediq/findex')
I'm running PostgreSQL 9.2
siq_query(...) is a server-side function taking 7 input parameters (or more). It's not part of any standard Postgres distribution I know (certainly not mainline Postgres 9.2), so it has to be user-defined or part of some extension you installed. It does whatever is defined in the function. This can include basically anything your Postgres user is allowed to do. Unless it's a SECURITY DEFINER function, then it ca do whatever the owner of the function is allowed to do.
The way it is called (SELECT * FROM), only makes sense if it returns multiple rows and/or columns, most likely a set of rows, making it a "set-returning function", which can be used almost like a table in SQL queries.
Since the function name is not schema-qualified, it has to reside in a visible schema. See:
How does the search_path influence identifier resolution and the "current schema"
Long story short, you need to see the function definition to know what it does exactly. You can use psql (\df+ siq_query), pgAdmin (browse and select it to see its definition in the SQL pane) or any other client tool to look it up. Or query the system catalog pg_proc directly:
SELECT * FROM pg_proc WHERE proname = 'siq_query';
Pay special attention to the column prosrc, which holds the function body for some languages like plpgsql.
There might be multiple variants of that name, Postgres allows function overloading.

Reduce bothering notices in plpgsql

I have a function which uses temporary table, that must be dropped if exists.
drop table if exists t_xy;
create temp table t_xy on commit drop as select ...;
Subsequently I use this function in a view. The function is called many times while select is in progress. I like to use "raise notice" command because it is almost the only reliable way to report any variables in functions for debug purposes. The problem is I must search for them in huge amount of unwanted lines like:
NOTICE: table "t_xy" does not exist, skipping
CONTEXT: SQL statement "drop table if exists t_xy"
PL/pgSQL function f_pending_operations(uuid) line5 in SQL command
Is there a way to suppress such notices that haven't been generated by raise notice command, but by drop table if exists or dropping other objects? Setting 'client_min_messages' option to 'debug' makes the problem worse.
You can silence notices to the client from any command with a local setting for client_min_messages:
SET LOCAL client_min_messages = warning; -- "debug" would have opposite effect
DROP TABLE if exists t_xy;
-- RESET client_min_messages;
If you don't issue RESET you effectively silence notices for the rest of the transaction. The manual:
The effects of SET LOCAL last only till the end of the current transaction
Alternatively, you can set client_min_messages in the call from the command line (for the duration of the session):
How to suppress INFO messages when running psql scripts
You can also reduce the verbosity of the messages using the GUC parameter:
set log_error_verbosity='terse';
which can of course be set at the function level.

Does wrapping a query in SELECT protect you completely in Postgres

Given a client library that can only execute one statement in a batch, if you run
query.exec_sql("SELECT * FROM (" + sql + ")")
Are there any vectors where sql can run anything but a SELECT ?
Are there any other ways to temporarily de-elevate a connection so it can only perform SELECT?
Note: It looks like SET ROLE solves this problem, but the the issue I have is that I am unable to create a role upfront in an easy way.
While you can put data-modifying statements in queries by embedding INSERT/UPDATE/DELETE statements in CTEs, they're only allowed at the top level, so that's not an issue.
You can, however, invoke a function, which could contain just about anything. Even if you ran this in a read-only transaction, a function could potentially elevate it to read-write.
But the solution is simple: If you don't want to allow the caller to do something, don't give them permission to do it. Create a user with only the GRANTs they need, and you can execute sql as-is.
Without the ability to define permissions, the closest you're going to get is probably a read-only transaction and/or an explicit rollback after the query, but there will still be holes you can't plug (e.g. you can't roll back a setval() call).
If the sql string came from a third party then it can be used to SQL injection. I'm not sure if that is what you are asking because it is too basic for a 56k points user to ask. Sorry if that is not the case. The string could be:
some_table; insert into user_table (user_id, admin_privilege) values (1, true);