Skip to content

Commit 6a76fe4

Browse files
committed
docs: rewrite recursion and max depth page for clarity
1 parent 752b661 commit 6a76fe4

File tree

3 files changed

+118
-83
lines changed

3 files changed

+118
-83
lines changed

app/spicedb/modeling/_meta.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export default {
33
"composable-schemas": "Composable Schemas (Preview)",
44
"representing-users": "Representing Users",
55
"validation-testing-debugging": "Validation, Testing, Debugging",
6-
"recursion-and-max-depth": "Recursion & Max Depth",
6+
"recursion-and-max-depth": "Cyclical Relationships",
77
"protecting-a-list-endpoint": "Protecting a List Endpoint",
88
"migrating-schema": "Migrating a Schema",
99
"access-control-management": "Access Control Management",
Lines changed: 116 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,68 @@
1-
# Recursion and Max Depth
1+
# Cyclical Relationships and Traversal Limits
22

3-
Permissions questions in SpiceDB are answered by traversing the **tree** constructed from the graph formed
4-
by combining the [schema] (structure) and [relationships] (data).
3+
SpiceDB answers permissions questions by traversing a **tree** constructed from your [schema] (structure) and [relationships] (data).
54

6-
A `CheckPermission` request will, for example, traverse starting from the resource+permission requested,
7-
along any referenced permissions and relations, until the subject is found or maximum depth is
8-
reached.
5+
When you call `CheckPermission`, SpiceDB starts at the resource and permission you specified, then walks through relations and permissions until it either finds the subject or determines the subject doesn't have access.
96

107
[schema]: /spicedb/concepts/schema
118
[relationships]: /spicedb/concepts/relationships
129

13-
## Max Depth
10+
## How Traversal Works
1411

15-
In order to prevent requests from traversing without bounds, SpiceDB comes with a defaults to a depth of
16-
`50`, after which computation is halted and an error is returned to the caller.
12+
Consider this simple example:
1713

18-
This max depth is configurable via the `--dispatch-max-depth` flag.
14+
```
15+
┌─────────────────────┐
16+
│ document:readme │
17+
│ permission: view │
18+
└─────────┬───────────┘
19+
│ viewer relation
20+
21+
┌─────────────────────┐
22+
│ group:engineering │
23+
│ permission: member │
24+
└─────────┬───────────┘
25+
│ member relation
26+
27+
┌─────────────────────┐
28+
│ user:alice │
29+
└─────────────────────┘
30+
```
31+
32+
When checking if `user:alice` can `view` `document:readme`, SpiceDB traverses:
33+
34+
1. `document:readme#view` → follows `viewer` relation
35+
2. `group:engineering#member` → follows `member` relation
36+
3. Found `user:alice` → returns **allowed**
37+
38+
Each arrow represents one "hop" in the traversal.
39+
This tree has a **depth of 3** (three nodes visited).
40+
41+
## Traversal Depth Limit
42+
43+
To prevent unbounded traversal, SpiceDB enforces a maximum depth limit.
44+
By default, this limit is **50 hops**.
45+
If a traversal exceeds this limit, SpiceDB returns an error rather than continuing indefinitely.
46+
47+
You can configure this limit with the `--dispatch-max-depth` flag:
48+
49+
```sh
50+
spicedb serve --dispatch-max-depth=100
51+
```
52+
53+
Most schemas work well within the default limit.
54+
You typically only need to increase it if you have legitimately deep hierarchies (like deeply nested folder structures).
1955

20-
## Recursion in Relationships
56+
## Cyclical Relationships (Cycles)
2157

22-
As a result of expecting the permissions graph to be a **tree**, SpiceDB _does not_ support recursive data dependencies that result in operations
23-
(such as `CheckPermission`) visiting the _same_ object more than once.
58+
A **cycle** occurs when traversing the permissions tree leads back to an object that was already visited.
59+
SpiceDB does not support cyclical relationships because the permissions graph must be a [tree], not a graph with loops.
2460

25-
### Example
61+
[tree]: https://zanzibar.tech/2SMVg4W_Wx:N:k
2662

27-
The following is an example of an **unsupported** nesting of groups:
63+
### Example of a Cycle
64+
65+
Consider this schema for nested groups:
2866

2967
```zed
3068
definition user {}
@@ -39,83 +77,72 @@ definition resource {
3977
}
4078
```
4179

42-
and relationships:
80+
With these relationships:
4381

4482
```
4583
resource:someresource#viewer@group:firstgroup#member
4684
group:firstgroup#member@group:secondgroup#member
4785
group:secondgroup#member@group:thirdgroup#member
48-
group:thirdgroup#member@group:firstgroup#member
86+
group:thirdgroup#member@group:firstgroup#member ← creates a cycle!
4987
```
5088

51-
When computing a permission answer for `resource:someresource`, SpiceDB will attempt this walk: `resource:someresource#viewer`
52-
-> `group:firstgroup#member` -> `group:secondgroup#member` -> `group:thirdgroup#member` ->
53-
`group:firstgroup#member` -> ..., causing a cycle.
54-
55-
## Common Questions
56-
57-
### Why doesn't SpiceDB simply support tracking the objects it has walked?
58-
59-
1. Nested recursive "sets" have unclear semantics.
60-
61-
2. Undesirable overhead.
62-
63-
#### Nested sets have semantics issues
89+
Visually, this creates a loop:
6490

65-
[Zanzibar] and ReBAC in general operate on _sets_: when a permission check is made, SpiceDB is
66-
answering whether the requested subject is a member of the _set_ formed of all subjects that are
67-
visited by walking the permissions tree.
68-
69-
[Zanzibar]: https://zanzibar.tech
70-
71-
The question becomes: if a group's members contains the members of _itself_, is that legal within
72-
a set?
73-
Much academic literature has been written about this topic (which we won't repeat here),
74-
but the very question raises whether allowing such an approach is semantically valid.
75-
76-
As a real example, imagine the following schema and relationships:
91+
```
92+
┌──────────────────────┐
93+
│ resource:someresource│
94+
│ permission: view │
95+
└──────────┬───────────┘
96+
│ viewer
97+
98+
┌──────────────────────┐
99+
│ group:firstgroup │◄─────────────────┐
100+
│ permission: member │ │
101+
└──────────┬───────────┘ │
102+
│ member │
103+
▼ │
104+
┌──────────────────────┐ │
105+
│ group:secondgroup │ │ member
106+
│ permission: member │ │ (cycle!)
107+
└──────────┬───────────┘ │
108+
│ member │
109+
▼ │
110+
┌──────────────────────┐ │
111+
│ group:thirdgroup │──────────────────┘
112+
│ permission: member │
113+
└──────────────────────┘
114+
```
77115

78-
```zed
79-
definition user {}
116+
When SpiceDB traverses this, it walks:
117+
`resource:someresource#viewer``group:firstgroup#member``group:secondgroup#member``group:thirdgroup#member``group:firstgroup#member` → ...
80118

81-
definition group {
82-
relation direct_member: user | group#member
83-
relation banned: user | group#member
84-
permission member = direct_member - banned
85-
}
86-
```
119+
The traversal returns to `group:firstgroup#member`, creating an infinite loop.
87120

88-
```
89-
group:firstgroup#direct_member@group:secondgroup#member
90-
group:firstgroup#banned@group:bannedgroup#member
91-
group:secondgroup#direct_member@user:tom
92-
group:bannedgroup#direct_member@group:firstgroup#member
93-
```
121+
### How SpiceDB Handles Cycles
94122

95-
As we see above,`user:tom` is a `direct_member` of `secondgroup`, which makes him a member
96-
of `firstgroup` -> which implies he's a member of `bannedgroup` -> which implies he's _not_
97-
a member of `firstgroup` -> thus making him no longer `banned` -> (logical inconsistency)
123+
SpiceDB does not have a dedicated cycle detector.
124+
Instead, when a cycle exists, the traversal continues looping until it hits the **maximum depth limit**, then returns an error.
125+
This same error occurs whether the cause is a cycle or simply a very deep (but acyclic) hierarchy.
98126

99-
Thus, to prevent the above issue from occurring, Zanzibar and other ReBAC implementations such
100-
as SpiceDB assume the permissions graph is a [tree].
127+
**Why not track visited objects?**
128+
SpiceDB intentionally avoids tracking visited objects for two reasons:
101129

102-
[tree]: https://zanzibar.tech/2SMVg4W_Wx:N:k
130+
1. **Semantic problems with self-referential sets**: When a group's members include itself, it creates logical paradoxes.
131+
For example, if a user is a member through a cycle but also banned through the same cycle, is the user a member or not?
103132

104-
#### Overhead
133+
2. **Performance overhead**: Tracking every visited object would require significant memory and network overhead, especially in distributed deployments.
105134

106-
From a practical perspective, tracking of visited objects when computing `CheckPermission` and
107-
other permissions queries results in having significant amount of overhead over the wire and in
108-
memory to track the full set of encountered objects and check for duplicates.
135+
## Common Questions
109136

110-
### What do I do about a max depth error on CheckPermission?
137+
### What do I do about a max depth error?
111138

112-
If you've received an error like:
139+
If you see an error like:
113140

114141
```
115142
the check request has exceeded the allowable maximum depth of 50: this usually indicates a recursive or too deep data dependency. Try running zed with --explain to see the dependency
116143
```
117144

118-
Run `zed --explain` with the parameters of the check to show whether the issue is due to recursion or because the tree is simply too deep:
145+
Use `zed permission check` with `--explain` to visualize the traversal path:
119146

120147
```sh
121148
zed permission check resource:someresource view user:someuser --explain
@@ -130,20 +157,28 @@ zed permission check resource:someresource view user:someuser --explain
130157
└── ! group:firstgroup member (cycle) (3.194125ms)
131158
```
132159

133-
### Why did my check work with recursion?
160+
The output shows each hop in the traversal.
161+
If you see `(cycle)` in the output, you have a cyclical relationship.
162+
If there's no cycle, your hierarchy is simply deeper than the limit allows.
163+
164+
### Why did my check succeed despite having a cycle?
134165

135-
SpiceDB automatically short-circuits `CheckPermission` operations when the target subject has been
136-
found.
166+
SpiceDB short-circuits `CheckPermission` when it finds the subject.
167+
If the subject is found before the traversal hits the cycle or exceeds the depth limit, the check succeeds.
137168

138-
If the subject was found before the maximum depth was hit, then the operation will complete
139-
successfully.
140-
_However_, if the subject was not found, SpiceDB will continue walking, and ultimately return
141-
the error you saw.
169+
However, if the subject is **not** found, the traversal continues until it hits the depth limit and returns an error.
142170

143-
### How do I check for a possible recursion when writing a relationship?
171+
### How do I prevent cycles when writing relationships?
172+
173+
Before writing a relationship that could create a cycle, use `CheckPermission` to verify the relationship won't create a loop.
174+
175+
For example, before writing `group:parent#member@group:child#member`, check if the parent is already reachable from the child:
176+
177+
```sh
178+
zed permission check group:child member group:parent
179+
```
144180

145-
Use the `CheckPermission` API to check if the subject contains the resource.
181+
If this check returns **allowed**, writing the relationship would create a cycle.
182+
If it returns **denied**, the relationship is safe to write.
146183

147-
For example, if writing the relationship `group:someparent#member@group:somechild#member` a check
148-
can be made for `group:somechild#member@group:someparent#member`: if the _parent_ has permission
149-
_on the child_, then the addition of this relationship will cause a recursion.
184+
This pattern works because: if the parent already has permission on the child, making the child a member of the parent creates a circular dependency.

app/spicedb/modeling/validation-testing-debugging/page.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ true
243243

244244
This command will also highlight which parts of the traversal were cached and if a [cycle] is detected.
245245

246-
[cycle]: ./recursion-and-max-depth#recursion-in-relationships
246+
[cycle]: ./recursion-and-max-depth#cyclical-relationships-cycles
247247
[tracing header]: #checkpermission-tracing-header
248248

249249
## SpiceDB GitHub Actions

0 commit comments

Comments
 (0)