hunk.code_window:              soPlanCacheOptions
                soResponseInternalErrorsConfig
                postPollHook
                _scSchemaCacheRef
                ekgStore
                soEnableRemoteSchemaPermissions
                soInferFunctionPermissions
                soConnectionOptions
                soWebsocketKeepAlive
  
hunk.labels: keep keep keep keep add keep keep keep keep
hunk.after_edit:              serverMetrics
  
hunk.file_path: server/src-lib/Hasura/App.hs
hunk.type: add
file: ## 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)
     ```
  
  