Authorization
Depot provides a flexible authorization model that allows you to control access to your data. Authorization can be configured at:
- Environment level and Dataset level
- Schema level
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 Cognitopre token generation lambdaand 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:
thisthe current value of the object - create:
afterthe value of the object assuming it could be created - update:
beforethe current value of the object andafterthe value of the object assuming it could be updated - delete:
beforethe 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
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.