Skip to main content

Authorization

Depot provides a flexible authorization model that allows you to control access to your data. Authorization can be configured at:

Depot authorization is based on basestar expressions where on of expression side is always the caller object. Caller object is parsed from the Authorization header and has two top level properties:

  • caller.id - unique identifier of the caller (Cognito user id)
  • caller.claims - claims are parsed from the JWT token and values to JWT token are added using Cognito pre token generation lambda and is outside of Depot responsibility. Claims structure can be extracted by parsing your bearer token using https://jwt.io/

Depot split access permissions into four sections:

  • read - read access to the object
  • write - write access to the object
  • update - update access to the object
  • delete - (soft)delete access to the object

If you do not specify a permission for a given schema operation, it as if you specified

anon: false
expression: true

To further limit access to authenticated users, permission expressions may be used.

Object level permissions

Object level permissions are specified in the schema file Object level permission are more effective than row level permissions, as they are evaluated before any data is fetched from storage. However, there is a tradeoff, as they are less flexible than row level permissions because you only have "static" expressions to work with.

Some examples of object level permissions:

MySchema:
properties:
owners:
set: string
permissions:
read:
expression: true
# All authenticated users can read these objects
create:
expression: "'role1' in caller.claims['roles']"
# Only users with 'role1' in their roles can create these objects there roles array is provided in the JWT token
update:
expression: "caller.claims['admin']"
# Only users that has admin:true can update these objects there admin is boolean provided in the JWT token
delete:
expression: "caller.claims['system:role'] == 'PO' || caller.claims['system:role'] == 'PM'"
# Only users that has system:role equal to PO or PM can delete these objects there system:role is string provided in the JWT token
# same result can be achieved using expression "caller.claims['system:role'] in ['PO', 'PM']"

Row level permissions

For row level permissions addition to caller, and per method additional objects are available to use in expressions:

  • read: this the current value of the object
  • create: after the value of the object assuming it could be created
  • update: before the current value of the object and after the value of the object assuming it could be updated
  • delete: before the current value of the object

Some examples of row level permissions:

MySchema:
properties:
owners:
set: string
guests:
set: user
type: string
permissions:
read:
expression: caller.id in this.owners || caller.id in this.guests
# All authenticated users can read these objects
create:
expression: "before.type in caller.claims['system:editor']"
# Only users with that has matching editor claim as object type property value can create these objects there system:editor is array of strings provided in the JWT token
update:
expression: caller.id in before.owners && caller.id in after.owners
# Only users with ids in owners array can update these objects there owners array is part of the object itself
delete:
expression: "before.type != 'system' && caller.claims['system:role'] == 'ADMIN'"
# Only on system objects can be deleted by ADMIN users
note

Provided examples are not complete and are only for demonstration purposes if you feel that you can't achieve your goal please contact Depot-team for help

Row level permission expands

All permissions support an expand field, which allows you to write permission expressions that take into account referenced and linked objects. Within a transaction, permissions that reference after states are calculated against a virtual view of the entire database - as if all of the writes succeeded (including inclusions using expand that reference other after states in the same transaction). If any permission cannot be asserted, then all of the writes fail. This allows you to write permisision expressions as assertions against the entire state of the data.

Some examples of row level permissions with expands:

Expand referenced object on create to check if caller is owner of the referenced object:

ns1.Project:
type: object
properties:
name:
type: string
owner:
type: User
expression: value ?? caller
ns1.Contribution:
type: object
properties:
project:
type: ns1.Project
subject:
type: string
permissions:
create:
expression: after.project.owner.id == caller.id
expand:
- after.project
read:
expression: 'true'

Expand referenced object on create to check if caller is one editor of the referenced object:

ns1.Project:
type: object
properties:
name:
type: string
editors:
type:
array: string

ns1.Contribution:
type: object
properties:
project:
type: ns1.Project
subject:
type: string
permissions:
create:
expression: caller.id in after.project.editors
expand:
- after.project
read:
expression: 'true'

Expand linked(one to one) object on create to check if caller is one of editors of the referenced object:


ns1.Editors:
type: object
properties:
project:
type: ns1.Project
editors:
type:
array: string

ns1.Project:
type: object
properties:
name:
type: string
links:
editors:
schema: ns1.Editors
single: true
expression: project.id == this.id

ns1.Contribution:
type: object
properties:
project:
type: ns1.Project
subject:
type: string
permissions:
create:
expression: caller.id in after.project.editors.editors
expand:
- after.project
- after.project.editors
read:
expression: 'true'

Pushdown of read permission expressions

Where possible in queries, read permission expressions are pushed down to the underlying storage, assuming this is possible, you get back the number of items you requested in count for a query. When it is not possible, the part(s) of the expression that cannot be pushed down are applied as a post-storage filter, in this case, items that do not match the expression are removed from the response list, and new items are not fetched. This means it is possible to get empty lists for a query, although it will have a paging token that allows you to fetch more (potentially empty) lists. To avoid this, ensure that read permission expressions make use of indexes and/or make sure that client queries do not ask for data they do not have permission for (or use client paging to step over missing values).

The reason that additional items are not fetched on the server is to remove a DoS vector.