rfcs/update-permission-check-condition.md
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.
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:
moderator or user 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 the user_role value to admin. 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.
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
filterand the updated row holds the condition specified withcheck.
Why introduce a check field in the update permission? Why not just apply the
insert permission's check condition on updates?
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:
check condition has to
hold true on the inserted row.filter condition holds true and
after the row is updated, the update permission's check 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
INSERTwithON CONFLICT DO UPDATEchecksINSERTpolicies'WITH CHECKexpressions only for rows appended to the relation by the INSERT path.
When an
INSERTcommand has an auxiliaryON CONFLICT DO UPDATEclause, if theUPDATEpath is taken, the row to be updated is first checked against theUSINGexpressions of anyUPDATEpolicies, and then the new updated row is checked against theWITH CHECKexpressions.
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 to WHEN in the
INSERT statement.check: not as straight forward, we'll need to know whether the row has
been inserted or updated so that we evaluate the correct check condition in
returning. This seems possible by checking a
system column
xmax (see this). So the returning
clause would probably look like:
returning
*,
IF (xmax = 0) THEN (insert's check condition) ELSE (update's check condition)