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 Todois readable if its parentListis not private.
- A Postis readable if the current user is a member of theOrganizationthat thePostbelongs 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.