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.