Best practice to handle column level select grants in PostGraphile - postgresql

PostGraphile does NOT recommend column-level SELECT grants, instead recommends to
split your concerns into multiple tables and use the
one-to-one relationship feature to link them.
Now I want my users table to have a role field that can be accessed by role_admin but not by role_consumer. Based on the above recommendation, I created two tables. users table (in public schema) contains all fields that both roles can see, and user_accounts (in private schema) contains role field that only role_admin must be able to see. role field is added to the user GraphQL type via computed columns.
CREATE SCHEMA demo_public;
CREATE SCHEMA demo_private;
/* users table*/
CREATE TABLE demo_public.users (
user_id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
);
/* user_accounts */
CREATE TABLE demo_private.user_accounts (
user_id INT PRIMARY KEY REFERENCES demo_public.users (user_id) ON DELETE CASCADE,
role text not null default 'role_consumer',
);
/* role as computed column */
CREATE FUNCTION demo_public.users_role
(
u demo_public.users
)
RETURNS TEXT as $$
<code>
$$ LANGUAGE SQL STRICT STABLE;
Now basically I have two potions to set permissions.
1) The first option is to use table level security. IOW to grant select access on table user_accounts to ONLY role_admin.
GRANT SELECT ON TABLE demo_private.user_accounts TO role_admin;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_admin;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_any_user_accounts ON demo_private.user_accounts FOR SELECT TO role_admin using (true);
The problem with this approach is that when role_consumer runs a query that contains role field
{
me {
firstname
role
}
}
The above query returns an error. This is not good since the error affect the whole result hiding the result of other sibling fields.
2) The other option is to use row level security besides table level; IOW on table level, to grant select access on table user_accounts to both role_admin and role_consumer but in row level only allow admins to access rows of user_accounts.
GRANT USAGE ON SCHEMA demo_private TO role_consumer;
GRANT SELECT ON TABLE demo_private.user_accounts TO role_consumer;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_consumer;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_user_accounts ON demo_private.user_accounts FOR SELECT
USING ('role_admin' = nullif(current_setting('role', true), ''));
Now if the user with consumer_role runs the aforementioned query, the role field will be null, not affecting its sibling fields. But two questions:
Should we always avoid errors to prevent them affecting their siblings?
If yes, should we always handle things in Row Level and never only in Table Level?

For option 1, throwing an error from PostgreSQL during a query is not a good idea in PostGraphile because we compile the entire GraphQL tree into a single SQL query, so an error aborts the entire query. Instead, I would factor the permissions into the function and simply return null (rather than an error) if the user is not allowed to view it. One way to do this is with an additional WHERE clause:
CREATE FUNCTION demo_public.users_role (
u demo_public.users
) RETURNS TEXT AS $$
select role
from demo_private.user_accounts
where user_id = u.id
and current_setting('jwt.claims.role') = 'role_admin';
$$ LANGUAGE SQL STABLE;
For option 2: this is a perfectly valid solution.
Should we always avoid errors to prevent them affecting their siblings?
It's rare to throw errors when querying things in GraphQL - normally you return null instead. Think of it like visiting a private repository on GitHub when logged out - they don't return the "forbidden" error which reveals that the resource exists, instead they return the 404 error suggesting that it doesn't - unless you know better!
If yes, should we always handle things in Row Level and never only in Table Level?
I personally only use one role with PostGraphile, app_visitor, and that has been sufficient for all applications I've built with PostGraphile so far.

Related

Why is the policy's "using"-clause used for the new row while a "with check"-clause is provided for an update statement?

From the Postgres CREATE POLICY documentation:
UPDATE policies accept both a USING expression and a WITH CHECK expression. The USING expression determines which records the UPDATE command will see to operate against, while the WITH CHECK expression defines which modified rows are allowed to be stored back into the relation.
See the following queries to reproduce an error.
-- model + data
create schema sandbox;
create table sandbox.person (
name text primary key,
deleted_at timestamp without time zone
);
insert into sandbox.person (name) values ('Bob'),('Alice');
-- user and permissions
create role tester;
grant usage on schema sandbox to tester;
grant select, insert, update, delete on table sandbox.person to tester;
-- RLS
alter table sandbox.person enable row level security;
create policy test_policy on sandbox.person
for all
to tester
using (deleted_at is null)
with check (true);
-- testing the policy
set role tester;
update sandbox.person set deleted_at = now()
where name = 'Alice';
--> ERROR: new row violates row-level security policy for table "person"
-- cleanup
reset role;
drop schema if exists sandbox cascade;
drop role if exists tester;
The error seems to be caused by the "using"-clause that is checked against the NEW row, but the documentation says that when a "with check"-clause is provided, the NEW row will be checked against the "with check"-clause instead of the "using"-clause. But according to the error I must be misinterpreting or overlooked a condition that invalidates the statement in the docs in this scenario.
Can you explain what I am missing and how the policy should look like? The end goal is to hide the records marked as deleted, and allow a user to mark them as such.
I reproduced my problem in Postgres 11 and 15.
Update
I've tried separating the policies into 1 for select and 1 for the update.
create policy test_policy_for_update on sandbox.person
for update
to tester
with check (true);
create policy test_policy_for_select on sandbox.person
for select
to tester
using (deleted_at is null);
This prevents the error in the update-statement but results in zero records updated.
Update 2
I now believe that the next best way to solves this is to create a separate update function with security definer to bypass RLS as proposed in this answer:
Postgres Row Level Security doesn't allow update on restrictive select policy
That proposal is due to the returning * added by postgraphile, but even without that, the separate update function solution is still needed.
You made your policy applying to all operations, including SELECT operations. The documentation states that even for an UPDATE statement there are cases where the USING clause of the SELECT policy is checked for the new row. This seems to be happening here.

Only allow read if user has exact document id postgresql row level security/supabase

Is there a way for a user to only be able to read a document only if they have the exact document ID?
I want to avoid creating users, so the only security is a random guid saved in browser memory - settings will be saved in "settings" table with id=guid.
So when page opens it will fetch with
supabase.from('settings').select('*').eq('id', guid)
How do I secure that setting (without creating (dummy) user)
Like this in Firebase:
Firebase firestore only allow read if user has exact document ID but for postgresql/supabase
This is doable, but I would:
Disallow all access to the table for anon users via RLS (return false from the RLS policy)
Write a postgres function using security definer that takes a uuid as a parameter and only returns a single row from the table based on that parameter. (return nothing if the row doesn't exist)
Call the function using the supabase .rpc() format.
Example:
create table people (id uuid primary key default gen_random_uuid(), name text);
alter table people enable row level security;
-- now, with no RLS policy, no anon or authenticated users can access the table
create or replace function get_person(person_id uuid)
return table (id uuid, name text) security definer
language sql AS $$
select id, name from people where id = person_id;
$$;
In your client code:
const { data, error } =
await supabase.rpc('get_person', { person_id: 'some-uuid' });
return { data, error };
Simply, you need to create a function in your current schema, which returning data from id and also you need to create only one user, that doesn't have any privilege except usage to that function.
Via this that person can use that function but can not select from your table.
For example codes: how-postgresql-give-permission-what-execute-a-function-in-schema-to-user

Postgresql Row Level Security Checking new data in WITH CHECK in Policy for INSERT

I've a Database with several tables.
A user has several objects and an object has several parts.
I want to write a policy that only the creator of the object is allowed to add parts to the object. Therefore I need to get the object a to be inserted part belongs to, but I've no idea how to check the data.
Is there a way to get the data to be inserted in the policy?
Thanks for your effort.
Here is an example how to implement something like that with row level security. Adapt it to your need!
CREATE TABLE object(
id integer PRIMARY KEY,
name text NOT NULL,
owner name NOT NULL DEFAULT current_user
);
CREATE TABLE part(
id integer PRIMARY KEY,
parent_id integer NOT NULL REFERENCES object(id),
name text NOT NULL
);
We have to give people some permissions:
GRANT SELECT, INSERT ON object TO PUBLIC;
GRANT SELECT, INSERT ON part TO PUBLIC;
Now we enable row level security and allow only INSERTs in part when the owner in object matches:
ALTER TABLE part ENABLE ROW LEVEL SECURITY;
CREATE POLICY insert_only_owned ON part
FOR INSERT TO PUBLIC
WITH CHECK (EXISTS(
SELECT 1
FROM object o
WHERE o.id = parent_id
AND owner = current_user
));

How do I protect specific columns from having their values explicitly set via a REST API update?

I have multiple tables that I would like users to be able to update through the rest api, and many (if not all) have columns with sensible defaults.
The web app itself can be designed to hide these columns, but I want to allow direct access to the api as well so that others can make use of the data however they see fit.
Unfortunately, this means they can set the defaulted columns explicitly (set timestamp columns to 1972, or set id columns to arbitrary values).
What mechanisms are available to restrict this on the backend (Postgres 9.4)?
You should do this at API level.
If anybody issues a malformed request (e.g. they want to overwrite an ID or a timestamp), answer with a proper status code (perhaps 400), amended with a meaningful message, for instance "Hey you tried to update , which is read only."
If you would really insist to handle it at db level, here they suggest that:
The easiest way is to create BEFORE UPDATE trigger that will compare OLD and NEW row and RAISE EXCEPTION if the change to the row is forbidden.
I've had some luck experimenting with Postgres' column-level grants. It's important in a development environment to make sure that your database users isn't a superuser (if it is, create a second superuser, then revoke it from the dev account with alter role).
Then, commands similar to these can be run on a table:
revoke all on schema.table from dev_user;
grant select, delete, references on schema.table to dev_user;
grant update (col1, col2) on schema.table to dev_user;
grant insert (col1, col2) on schema.table to dev_user;
Some caveats:
Remember to grant "references" as well if another table will fkey to it.
Remember to give col1 and col2 (and any other) sane defaults, because the API will be unable to change those in any way.
DO NOT FORGET TO CREATE A SECOND SUPERUSER ACCOUNT BEFORE REVOKING SUPERUSER STATUS FROM THE DEV ACCOUNT. It is possible to recover this, but a big pain in the ass.
Also, if you're keeping these grant/revocations in the same file as the create table statement, the following form might be of use:
do $$begin execute 'grant select, delete, references on schema.table to ' || current_user; end$$;
This way the statements will translate correctly to production, which may not use the same username as in development.
PostgreSQL since version 9.3 supports updatable views, so instead of exposing actual table you can expose a view with a limited subset of columns:
CREATE TABLE foo (id SERIAL, name VARCHAR, protected NUMERIC DEFAULT 0);
CREATE VIEW foo_v AS SELECT name FROM foo;
Now you can do things like:
INSERT INTO foo_v VALUES ('foobar');
UPDATE foo_v SET name = 'foo' WHERE name = 'foobar';
If you need more you can use INSTEAD INSERT/UPDATE RULE or INSTEAD OF INSERT TRIGGER.

PostgreSQL SELECT-RULES , inheritance, row-level permissions

Here's what I've been reading:
http://www.postgresql.org/docs/9.2/static/rules-views.html
http://www.postgresql.org/docs/9.2/static/rules-privileges.html
My goal is to allow a login to see only those rows that it "owns", so to speak.
Let's say every table in the database inherits from this table:
create table WHOAMI
(
tenant varchar(25) not null default current_user
);
for example:
create table FOO
(
id int primary key,
invoicedate date
) inherits (WHOAMI);
insert into FOO(id, invoicedate) values(1,now()::date);
select * from FOO;
--abclogin|1|2013-02-01
Is there such a thing in PostgreSQL as a schema-level select rule, affecting all tables and views in the schema, that appends to every select, insert, update, or delete statement a condition that says, in effect, ..AND WHERE TENANT = current_user? If there isn't such a global rule, can it be done on a table-by-table basis? I am not having any success with my attempts, and am probably misunderstanding a few things about how rules are created. Here is what I have tried to do:
I try to create a select-rule:
CREATE RULE "_RETURN" AS ON SELECT TO FOO DO INSTEAD
SELECT * FROM FOO where tenant = current_user;
but get this error: ERROR: could not convert table "foo" to a view because it has indexes
I try to create a view with a security-barrier:
CREATE VIEW TENANTFOO WITH (security_barrier) AS
SELECT * FROM FOO WHERE tenant=current_user;
and then attempt an insert:
insert into TENANTFOO(id,invoicedate)
values(2,(now()::date);
but get this error:
`ERROR: cannot insert into view "tenantfoo"
HINT: You need an unconditional ON INSERT DO INSTEAD rule
or an INSTEAD OF INSERT trigger.`
What steps are required to implement row-level security barriers on tables?
In your last example, you'd need to run the INSERT against the table or create another RULE: ON INSERT TO TENANTFOO DO INSTEAD.
What you're looking for is a Row-Level Security, it is not yet available, although some work had been done on this thing. I hope this patch will make it into the upcoming 9.3 release.
Meanwhile, I've been working with the following design a while ago.
Requirements were similar, views should have been delivering only those rows intended for the CURRENT_USER. In our case access had been done quite simple: a table that specified whether given user had access for the given relation and given key, smth like:
CREATE TABLE user_grants (
user_id integer,
entity_name text, -- should exist in pg_class
entity_id integer
);
Then, say for the tasks, the following view had been created:
CREATE VIEW tasks_v AS
SELECT t.*
FROM tasks t
JOIN user_grants ug ON t.user_id = ug.user_id
AND ug.entity_name='TASKS' AND ug.entity_id = t.task_id;
Of course, the setup is not complete without a number of helper functions, triggers and rules. Also it was necessary to make sure some reasonable default privileges are always granted.