Working With Relations
In the previous chapters, we've learned how to write basic policy rules involving model fields and the current user. However, real-world applications usually have authorization requirements that need to access relations. Here're some examples:
- A
Todo
is readable if its parentList
is not private. - A
Post
is readable if the current user is a member of theOrganization
that thePost
belongs to.
In this chapter, we'll learn how to write policy rules that involve relations.
*-to-one Relations
Accessing *-to-one relation is straightforward. You simply reference the field directly. You can further visit the relation's fields using the dot notation.
model List {
id Int
private Boolean
}
model Todo {
id Int
list List @relation(...)
// `list` references a to-one relation
@@allow('update', !list.private)
}
You can chain the dot notation to make deep traversal in the relation graph. There's no limit on how deep you can go.
*-to-many Relations
To-many relations are a bit more complicated because you're dealing with a list of objects instead of one. The authorization conditions written with to-many relations can be one of the following three forms:
- Does any of the objects in the list satisfy the condition?
- Does all of the objects in the list satisfy the condition?
- Does none of the objects in the list satisfy the condition?
ZenStack provides a succinct syntax called "Collection Predicate Expression" for writing such rules:
Any: relation?[condition]
All: relation![condition]
None: relation^[condition]
The relation
part refers to a to-many relation field. The condition
part is a boolean expression scoped to the type of relation
. I.e., it can reference the member fields of relation
without any qualification.
A collection predicate expression must start with a to-many relation field. You can't use a simple array field (like String[]
) with it. In the next chapter you'll learn about helper functions that help you work with simple arrays.
🛠️ Adding Relation-Based Access Control
Let's continue working on the schema of our Todo app and add policies that control data access for users within the same space.
model Space {
...
// require login
@@deny('all', auth() == null)
// everyone can create a space
@@allow('create', true)
// users in the space can read the space
@@allow('read', members?[user == auth()])
// space admin can update and delete
@@allow('update,delete', members?[user == auth() && role == 'ADMIN'])
}
model SpaceUser {
...
// require login
@@deny('all', auth() == null)
// space owner and admins have full access
@@allow('all', space.owner == auth() || space.members?[user == auth() && role == 'ADMIN'])
// user can read members of spaces that he's a member of
@@allow('read', space.members?[user == auth()])
}
model User {
...
// everyone can sign up
@@allow('create', true)
// full access by oneself
@@allow('all', auth() == this)
// can be read by users sharing any space
@@allow('read', spaces?[space.members?[user == auth()]])
}
model List {
...
// require login
@@deny('all', auth() == null)
// can be read by space members if not private
@@allow('read', owner == auth() || (space.members?[user == auth()] && !private))
// when create, owner must be set to current user, and user must be in the space
@@allow('create,update', owner == auth() && space.members?[user == auth()])
// can be deleted by owner
@@allow('delete', owner == auth())
}
model Todo {
...
// require login
@@deny('all', auth() == null)
// owner has full access
@@allow('all', list.owner == auth())
// space members have full access if the parent List is not private
@@allow('all', list.space.members?[user == auth()] && !list.private)
}
Let's test it out. Rerun generation and start REPL:
npx zenstack generate
npx zenstack repl
Switch to user Joey and fetch Todo Lists:
.auth { id: 1 }
db.list.findMany()
[
{
id: 1,
createdAt: 2023-11-08T04:38:53.385Z,
updatedAt: 2023-11-08T04:38:53.385Z,
spaceId: 1,
ownerId: 2,
title: 'Grocery',
private: false
}
]
We queried with user Joey and now can get the List
created by Rachel because Joey is a member of the same space.