Skip to content

Commit 2625001

Browse files
authored
blog: check function (#328)
* blog: check function * update
1 parent e4816fe commit 2625001

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

blog/check-function/cover.jpg

431 KB
Loading

blog/check-function/index.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
---
2+
title: How the "check" Function Helps Keep Your Policies DRY
3+
description: This post introduces a typical pattern of access policy duplication in ZenStack schemas, and explains how the new `check` attribute function can help you keep your policies DRY.
4+
tags: [zenstack]
5+
authors: yiming
6+
date: 2024-07-13
7+
image: ./cover.jpg
8+
---
9+
10+
# How the "check" Function Helps Keep Your Policies DRY
11+
12+
![Cover Image](cover.jpg)
13+
14+
Among ZenStack's features, the most beloved one is the ability to define access control policies inside the data schema. This ensures that your rules are colocated with the source code, always in sync with the data model, and easy to understand. It arguably provides a superior DX to other solutions like hand-coded authorization logic, or Postgres row-level security.
15+
16+
However, as your application grows more complex, you may find yourself repeating the same policy patterns across multiple models. This post explores one typical pattern of such duplication and demonstrates how the new `check()` attribute function can help you keep your policies DRY.
17+
18+
<!--truncate-->
19+
20+
## The parent-child duplication pattern
21+
22+
Consider a simple Todo application with two models: `List` and `Todo`. Each list can have multiple todos. The author of a list has full access to it. A list can also be set as public so anyone can read it. A todo's access control is determined by its containing list: one has the same permissions to a todo as to its parent.
23+
24+
Here's how a ZModel schema for the application might look:
25+
26+
```zmodel
27+
model List {
28+
id Int @id
29+
name String
30+
public Boolean
31+
author User @relation(fields: [authorId], references: [id])
32+
authorId Int
33+
todos Todo[]
34+
35+
// highlight-start
36+
@@allow('all', auth() == author)
37+
@@allow('read', public)
38+
// highlight-end
39+
}
40+
41+
model Todo {
42+
id Int @id
43+
name String
44+
list List @relation(fields: [listId], references: [id])
45+
listId Int
46+
47+
// highlight-start
48+
@@allow('all', auth() == list.author)
49+
@@allow('read', list.public)
50+
// highlight-end
51+
}
52+
```
53+
54+
As you can easily spot, the access policies for `Todo` are almost identical to those for `List`. The duplication is not only tedious to write but also error-prone to maintain. If you ever need to change the policy, you have to remember to update it in two places. In a real application, the rules will be more complex, and as a result, the duplication will be more severe and can sometimes get several level deep.
55+
56+
The key to the problem is that from the access control point of view, the child model `Todo` simply "follows" the parent model `List`, which is a typical pattern in many applications. We can avoid duplication if we had a way to "delegate" the check of the child model to its parent.
57+
58+
## The `check()` function
59+
60+
The new `check()` attribute function introduced in ZenStack [v2.3](https://github.com/zenstackhq/zenstack/releases/tag/v2.3.0) is designed exactly for such "delegation". The function has the following signature:
61+
62+
```ts
63+
function check(field: FieldReference, operation String?): Boolean
64+
```
65+
66+
- `field`
67+
68+
A relation field to check access for. Must be a non-array field.
69+
70+
- `operation`
71+
72+
An optional argument indicating the CRUD permission kind to check. If the operation is not provided, it defaults to the operation of the policy rule containing it.
73+
74+
You can use the function in a few different forms:
75+
76+
1. You can explicitly specify the kind of CRUD operation to delegate to:
77+
78+
```zmodel
79+
model Child {
80+
parent Parent
81+
@@allow('read', check(parent, 'update'))
82+
}
83+
```
84+
85+
2. Or you can omit the operation so it defaults to the current context:
86+
87+
```zmodel
88+
model Child {
89+
...
90+
parent Parent
91+
92+
@@allow('read', check(parent)) // here the operation is implicitly 'read'
93+
}
94+
```
95+
96+
3. You can also delegate "all" operations:
97+
98+
```zmodel
99+
model Child {
100+
...
101+
parent Parent
102+
103+
@@allow('all', check(parent))
104+
}
105+
```
106+
107+
The above is equivalent to:
108+
109+
```zmodel
110+
model Child {
111+
...
112+
parent Parent
113+
114+
@@allow('read', check(parent))
115+
@@allow('create', check(parent))
116+
@@allow('update', check(parent))
117+
@@allow('delete', check(parent))
118+
}
119+
```
120+
121+
4. Since `check` is just a boolean function, you can freely combine it with other conditions:
122+
123+
```zmodel
124+
model Child {
125+
...
126+
parent Parent
127+
128+
@@allow('read', check(parent) || auth().status == 'PAID')
129+
}
130+
```
131+
132+
## Before and after
133+
134+
With this new weapon in hand, we can easily refactor our Todo schema to eliminate the duplication:
135+
136+
```zmodel
137+
model List {
138+
id Int @id
139+
name String
140+
public Boolean
141+
author User @relation(fields: [authorId], references: [id])
142+
authorId Int
143+
todos Todo[]
144+
145+
@@allow('all', auth() == author)
146+
@@allow('read', public)
147+
}
148+
149+
model Todo {
150+
id Int @id
151+
name String
152+
list List @relation(fields: [listId], references: [id])
153+
listId Int
154+
155+
// highlight-next-line
156+
@@allow('all', check(list))
157+
}
158+
```
159+
160+
Pretty neat, isn't it?
161+
162+
## What's next?
163+
164+
Having the `check` function is great for resolving the parent-child duplication pattern. However, there are more related features that can be explored in future versions of ZenStack. Here are some ideas:
165+
166+
### 1. `*-to-many` relations
167+
168+
You've probably already noticed the limitation: the `check` function only works for the `*-to-one` side of a relation. Although I feel it should cover most of the use cases, there might be cases where you want to deal with the "*-to-many" side.
169+
170+
There are two possible ways to add such support:
171+
172+
- A: a new set of check functions: `checkSome`, `checkAll`, `checkNone`, etc.
173+
174+
```zmodel
175+
model Parent {
176+
children Child[]
177+
178+
@@allow('read', checkSome(children, 'read'))
179+
}
180+
```
181+
182+
- B: make it work with [collection predicate expressions](../docs/reference/zmodel-language#collection-predicate-expressions)
183+
184+
```zmodel
185+
186+
model Parent {
187+
children Child[]
188+
189+
// check if at least one child is readable
190+
@@allow('read', children?[child, check(child, 'read')])
191+
}
192+
193+
```
194+
195+
What's your preference? Leave a comment below!
196+
197+
### 2. Recursive relations
198+
199+
The `zenstack` CLI checks for cycles in the `check` call graph and reports an error if it finds one. This is needed to prevent infinite loops during evaluation. However, there are cases where cyclic delegation is intentionally needed, especially in the case of recursion. For example, if you want to model a Google Drive-like system, you may end up with a recursion like this:
200+
201+
```zmodel
202+
model Folder {
203+
parent Folder?
204+
children Folder[]
205+
permissions Permission[]
206+
207+
// a folder is readable if it's configured with a permission or if its parent is readable
208+
@@allow('read', check(parent) || permissions?[type == 'read' && user == auth()])
209+
}
210+
```
211+
212+
This will be a hard problem to solve since Prisma inherently doesn't support recursive queries (check [this issue](https://github.com/prisma/prisma/issues/3725) for details). A possible solution is to expand the recursion with a (configurable) finite levels of depth.
213+
214+
Is this something your app needs?
215+
216+
### 3. Other forms of duplication?
217+
218+
Parent-child is just one of the patterns of duplication out there. Are there other patterns hurting you? Leave a comment or join our [discord server](https://discord.gg/Ykhr738dUe) to discuss! We love real-world problems and our innovation never stops.

0 commit comments

Comments
 (0)