I have a table like:
table person_groups
person_id: uuid
group_id: uuid
and I want people for whom uid() = person_id to be able to get all of the other person_ids in all of their groups.
I tried to use
CREATE POLICY "select_comembers" ON "public"."person_groups"
AS PERMISSIVE FOR SELECT
TO public
USING (group_id in (select group_id from person_groups where person_id=uid()))
but this triggers an infinite recursion error when RLS is enforced (at edit time, the definer isn't subject to RLS, so no in that case this is fine apparently).
What's the appropriate way to allow finding all of the rows in a table that share a different column to the one you're filtering on, in this way?
Is there an appropriate way to use JOINs to solve this?
That sounds like you should redesign your data model. A possible solution would be to replace the subquery with a call to a SECURITY DEFINER function that belongs to the table owner or another user that is exempt from row-level security.
Related
In this example, the second column should not be visible for a member (role) of the group 'user_group', because this column is only required internally to regulate the row level security. however, records can only be deleted if this column is also visible. How can you get around that?
Options that come to mind would be:
just make the second column visible (i.e. selectable), which would
actually be completely superfluous and I want to hide internally as
much as possible
write a function that is called with elevated rights
(security definer), which I want even less.
Are there any other options?
(and especially with deletions I want to use nice things like 'ON DELETE SET NULL' for foreign keys in other tables, instead of having to unnecessarily program triggers for them)
create table test (
internal_id serial primary key,
user_id int not null default session_user_id(),
info text default null
);
grant
select(internal_id, info),
insert(info),
update(info),
delete
on test to user_group;
create policy test_policy on policy for all to public using (
user_id = session_user_id());
RLS just implicitly adds unavoidable WHERE clauses to all queries, it doesn't mess with the roles under which code is evaluated. From the docs:
"Since policy expressions are added to the user's query directly, they will be run with the rights of the user running the overall query. Therefore, users who are using a given policy must be able to access any tables or functions referenced in the expression or they will simply receive a permission denied error when attempting to query the table that has row-level security enabled."
This feature is orthogonal to the granted column permissions. So the public role must be able to view the user_id column, otherwise evaluating user_id = session_user_id() leads to an error. There's really no way around making the column visible.
completely superfluous and I want to hide internally as much as possible
The solution for that would be a VIEW that doesn't include the column. It will even be updatable!
Because of a security concern, I need to hide the information about a few IDs from some users in my PostgreSQL database (9.5). The info are distributed in a number of tables, and can be in new tables in future. Can I create a database wide security policy to do that?
I checked the row level security command, e.g., CREATE POLICY name ON table_name TO role_name USING id not in (). But seems that only applies to specific tables. Any suggestion?
With a loop, it is possible to enable RLS for multiple tables at once.
A policy has to be applicable to all those tables, if you want to define it for multiple ones. If you use a custom constraint behind 'public' you can filter the tables to which your policy applies.
DO
$$
DECLARE
row record;
BEGIN
FOR row IN SELECT tablename FROM pg_tables AS t
WHERE t.schemaname = 'public' -- Add custom filter here, if desired.
LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', row.tablename); -- Enable RLS for tables
EXECUTE format('CREATE POLICY some_policy ON %I TO some_user
USING (some_attribute = some_value;', row.tablename); -- Only works if all tables share "some_attribute"
END LOOP;
END;
$$;
Please note:
I was inspired by this answer: How to change schema of multiple PostgreSQL tables in one operation?
The combination of loop with RLS may be an insecure practice. I do not think so, but I do not know for sure, because I did not work with loops before.
The policy you define has to be able to work on all tables. If it applies to most tables, not all: You may exclude the non-working ones before. Sometimes, there is e.g. a userId which is shared among most tables.
In the specific question some_attribute = ... would be similar to the example inside offical docs, where they check for user_name = current_user.
To find an example where this answer was useful, see this question of mine.
Yes, row level security has to be enabled per table, and policies have to be defined for each table. There is no way around that.
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.
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.
I want to create some roles in my Postgresql DB and grant some access.
I have student role and i want to grant this user type : can edit only record a bout him/her in student table and can not edit other rows
how can i do it?
thanks
Create a view on the table with an appropriate where clause, then grant access to that:
create view students_view as
select col1, col2, col3 -- limit column access here
from mytable
where <whatever>; -- limit row access here
-- limit what he can do here
grant update, select to student_role;
BTW It is a commonly held misconception that you can't update a view, but that is only true if the view is a join or similarly complicated query.
PostgreSQL doesn't have row-level declarative security (yet, there's ongoing work into it) so if you can't just create a view - say, if you have many different people who need this access - you will probably need a SECURITY DEFINER helper function or trigger.
You've got a couple of options:
Write a SECURITY DEFINER function that lets them make only the permitted changes and limit their access to the table to SELECT, revoking UPDATE, DELETE, TRUNCATE and INSERT rights; or
write a trigger that tries to restrict them from making changes you don't want them to make and GRANT them write access to the table.
Of the two, the function and restricted rights approach is by far the safest option so long as you follow the SECURITY DEFINER secure coding guidelines set above - setting search_path for the function, avoiding dynamic SQL (EXECUTE) with string substitutions, etc.
The view approach given above can work quite nicely if it's a view that filters by current_user. You may also want to look at the new SECURITY BARRIER views; see this post for a useful discussion of them.
GRANT UPDATE(column) on tabela to user_name;