diff --git a/rfcs/update-permission-check-condition.md b/rfcs/update-permission-check-condition.md new file mode 100644 index 00000000000..3da50485603 --- /dev/null +++ b/rfcs/update-permission-check-condition.md @@ -0,0 +1,152 @@ +## 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: + +```sql +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: +1. 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. +2. If a 'user' is a 'moderator' of a workspace, they can only add other users + with `moderator` or `user` role and the updates too are restricted to these roles. + +The `insert` permission on `workspace_membership` will be as follows: + +```json +{ + "_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`: + +1. 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: + ```json + { + "workspace": { + "members": { + "user_id": {"_eq": "x-hasura-user-id"}, + "user_role": {"_in": ["admin", "moderator"]} + } + } + } + ``` + +2. 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. + + +### 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 with `check`. + +#### Other options considered: + +- Why introduce a `check` field in the update permission? Why not just apply the + insert permission's `check` condition on updates? + + 1. It may not make sense to allow inserts, but a check condition on update needs + to be specified. + 2. 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: + +1. When there is no conflict, the insert permission's `check` condition has to + hold true on the inserted row. +2. 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's `check` condition has to + hold true. + +This is pretty much what Postgres does while enforcing [RLS policies](https://www.postgresql.org/docs/current/sql-createpolicy.html). +The relevant parts from the above doc are: + +> Note that `INSERT` with `ON CONFLICT DO UPDATE` checks `INSERT` policies' +`WITH CHECK` expressions only for rows appended to the relation by the INSERT path. + +> When an `INSERT` command has an auxiliary `ON CONFLICT DO UPDATE` clause, +if the `UPDATE` path is taken, the row to be updated is first checked against +the `USING` expressions of any `UPDATE` policies, and then the new updated row +is checked against the `WITH 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? + +1. `filter`: we already do this, by adding the condition to `WHEN` in the + `INSERT` statement. +2. `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](https://www.postgresql.org/docs/current/ddl-system-columns.html) + `xmax` (see [this](https://stackoverflow.com/q/34762732)). So the `returning` + clause would probably look like: + ```sql + returning + *, + IF (xmax = 0) THEN (insert's check condition) ELSE (update's check condition) + ``` +