How would one go about defining extended permissions and authorization on a many-to-many relationship using Postgraphile?
Imagine I have this many-to-many relationship (graciously borrowed from official docs and adjusted with simplified permissions):
create table post (
id serial primary key,
headline text,
body text,
summary text
);
create table author (
id serial primary key,
name text
);
create type post_permission as enum (
'owner', -- assume owner can change `headline`, `summary` and `body` of a post
'editor' -- assume editor can modify `summary` of a post
);
create table post_author (
post_id integer references post,
author_id integer references author,
permission post_permission,
primary key (post_id, author_id)
);
I can make a Row Level Security Policy like:
create policy update_post on post for update to app_user using (
EXISTS(
SELECT 1
FROM post_author as pa
WHERE pa.post_id = post.id
AND pa.author_id = app_hidden.current_user_id()
AND pa.permission = 'owner'
)
);
-- Assume `app_hidden.current_user_id()` returns a logged in user id
But as I am a recent MySQL convert to PostgreSQL I am trying to see if I can make the policy check pa.permission above in relation to the attempted change and only allow permission = owner to update all fields of a post, whereas a user with permission = editor can just update summary.
I am aware that this is often handled in the app layer and not database, but figured I would see whats possible first.
Thanks!
See also related topic here.
Based on investigation and trial-and-error, this seems to be something that is best solved with a custom function for updating posts.
An owner can use this function via GraphQL/Postgraphile:
create function updatePost(
headline text,
body text,
summary text
) returns post as $$
-- implement this function to check that the user found via
-- app_hidden.current_user_id() exists in join table
-- with an `owner` permission
-- then modify post
$$ language plpgsql strict security definer;
An editor can use this function via GraphQL/Postgraphile:
create function updatePostMeta(
summary text
) returns post as $$
-- implement this function to check that the user found via
-- app_hidden.current_user_id() exists in join table
-- with an `editor` or `owner` permission
-- then modify post
$$ language plpgsql strict security definer;
Additionally, using RLS, one would want to prevent anyone from changing a post directly via GraphQL/Postgraphile, so we'd only let users SELECT from post
Related
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
I have a Database that is meant to store Articles and Tags. Tags are created automatically with new Articles, to make handling them easier. The max amount of tags is limited to 5 by the backend. Heres what the tables look like:
CREATE TABLE tag(
name text PRIMARY KEY,
created timestamp NOT NULL DEFAULT NOW()
);
CREATE TABLE article(
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
name text,
description text,
author text references profile(name)
);
CREATE TABLE tag_article(
tag text REFERENCES tag(name),
article uuid REFERENCES article(id),
UNIQUE (tag, article)
);
I have created a stored procedure to create the article, create all the tags (unless they exist) and then connect them using the tag_article table:
create or replace procedure create_article(
author text,
name text,
description text,
tags text[]
)
language plpgsql
as $$
begin
INSERT INTO tag(name)
SELECT v
FROM unnest(tags) g(v)
ON CONFLICT DO NOTHING;
WITH article(id) AS(
INSERT INTO article(author, name, description)
values(author,name,description)
RETURNING id
)
INSERT INTO tag_article(tag,article)
SELECT g.v, article.id FROM
unnest(tags) g(v), article;
COMMIT;
END $$;
Now I need a way to return the tags that have been created (if there were any) and the article. I know for the tags I can just use returning *, since that will give me only the created tags.
But I also need a way to forward this to the caller of the procedure. I also need to do the same with the article. The problem there is, I am already returning the id, so I don't know how I could just return the entire row with it.
So how can I modifiy this code in order to
Return all the newly created tags, including the creation date and other information that might later be added to the tag table or just an empty set
Return the Article, which may also receive more attributes
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'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
));
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.