5.2 KiB
Update permissions: Allow checking a condition on an updated row
Our insert permissions allow checking a condition on an inserted row but
update permissions only allow restricting updates to a set of rows
(with filter
) - there is no means to check a condition on the updated row.
Motivation
Consider this schema for a slack like application:
create table slack_user (
id serial primary key,
name text not null
);
create table workspace (
id serial primary key,
name text not null
);
create table workspace_membership (
id serial primary key,
workspace_id integer references workspace (id),
user_id integer references slack_user (id),
user_role text not null
);
Let's say a user can have 3 kinds of roles for a workspace, 'admin', 'moderator'
and 'user' (modelled with role
column in workspace_membership
). The permissions
for workspace_membership
table are as follows:
- If a 'user' is an 'admin' of a workspace, they can add any other user to the workspace with any role or modify the membership of any user.
- If a 'user' is a 'moderator' of a workspace, they can only add other users
with
moderator
oruser
role and the updates too are restricted to these roles.
The insert
permission on workspace_membership
will be as follows:
{
"_or": [
{
"workspace": {
"members": {
"user_id": {"_eq": "x-hasura-user-id"},
"user_role": {"_eq": "admin"}
}
}
},
{
"workspace": {
"members": {
"user_id": {"_eq": "x-hasura-user-id"},
"user_role": {"_eq": "moderator"}
}
},
"user_role": {
"_in": ["user", "moderator"]
}
}
]
}
Let's try specifying an update
permission on workspace_membership
:
-
What are the set of rows that can be modified by a user?
The rows where the user is a 'moderator' or an 'admin' of the workspace. So, it would be:
{ "workspace": { "members": { "user_id": {"_eq": "x-hasura-user-id"}, "user_role": {"_in": ["admin", "moderator"]} } } }
-
What columns can be updated?
An admin or a moderator should be able to modify the
user_role
column. However, if we allow modifying this column, a moderator can set theuser_role
value toadmin
. So we will also need to check a condition (in this case, same as insert's check condition) on the updated row.This is currently missing, we'll need to add an insert permission's
check
condition feature for update permissions too.
Proposed change
Update permission will have a new field called "check"
which takes as boolean
condition, similar to insert permission. The semantics will be as follows:
A row is only updated if the row is allowed to be updated with
filter
and the updated row holds the condition specified withcheck
.
Other options considered:
-
Why introduce a
check
field in the update permission? Why not just apply the insert permission'scheck
condition on updates?- It may not make sense to allow inserts, but a check condition on update needs to be specified.
- The check conditions maybe different for both insert and update permisisons.
Implementation
In case of update mutations, the check
condition can be checked the same way as how
insert's check condition is checked, by making it part of returning
. The
tricky part would be the behaviour when on_conflict
is used:
- When there is no conflict, the insert permission's
check
condition has to hold true on the inserted row. - When there is a conflict, the update should only happen if the row can be
updated, i.e, the update permission's
filter
condition holds true and after the row is updated, the update permission'scheck
condition has to hold true.
This is pretty much what Postgres does while enforcing RLS policies. The relevant parts from the above doc are:
Note that
INSERT
withON CONFLICT DO UPDATE
checksINSERT
policies'WITH CHECK
expressions only for rows appended to the relation by the INSERT path.
When an
INSERT
command has an auxiliaryON CONFLICT DO UPDATE
clause, if theUPDATE
path is taken, the row to be updated is first checked against theUSING
expressions of anyUPDATE
policies, and then the new updated row is checked against theWITH CHECK
expressions.
filter
and check
in our permissions are modelled after USING
and CHECK
in RLS. How do we enforce update permission's filter
and check
conditions
without having access to low level interfaces like Postgres does?
filter
: we already do this, by adding the condition toWHEN
in theINSERT
statement.check
: not as straight forward, we'll need to know whether the row has been inserted or updated so that we evaluate the correctcheck
condition inreturning
. This seems possible by checking a system columnxmax
(see this). So thereturning
clause would probably look like:returning *, IF (xmax = 0) THEN (insert's check condition) ELSE (update's check condition)