From 6a76fe4bf175ab3c7b6f9079f1bfa2b2d72c447f Mon Sep 17 00:00:00 2001 From: ivanauth Date: Fri, 6 Feb 2026 11:43:59 -0500 Subject: [PATCH] docs: rewrite recursion and max depth page for clarity --- app/spicedb/modeling/_meta.ts | 2 +- .../modeling/recursion-and-max-depth/page.mdx | 197 +++++++++++------- .../validation-testing-debugging/page.mdx | 2 +- 3 files changed, 118 insertions(+), 83 deletions(-) diff --git a/app/spicedb/modeling/_meta.ts b/app/spicedb/modeling/_meta.ts index 6935bd8c..79cbf2f2 100644 --- a/app/spicedb/modeling/_meta.ts +++ b/app/spicedb/modeling/_meta.ts @@ -3,7 +3,7 @@ export default { "composable-schemas": "Composable Schemas (Preview)", "representing-users": "Representing Users", "validation-testing-debugging": "Validation, Testing, Debugging", - "recursion-and-max-depth": "Recursion & Max Depth", + "recursion-and-max-depth": "Cyclical Relationships", "protecting-a-list-endpoint": "Protecting a List Endpoint", "migrating-schema": "Migrating a Schema", "access-control-management": "Access Control Management", diff --git a/app/spicedb/modeling/recursion-and-max-depth/page.mdx b/app/spicedb/modeling/recursion-and-max-depth/page.mdx index 58cd1d4f..965ed80d 100644 --- a/app/spicedb/modeling/recursion-and-max-depth/page.mdx +++ b/app/spicedb/modeling/recursion-and-max-depth/page.mdx @@ -1,30 +1,68 @@ -# Recursion and Max Depth +# Cyclical Relationships and Traversal Limits -Permissions questions in SpiceDB are answered by traversing the **tree** constructed from the graph formed -by combining the [schema] (structure) and [relationships] (data). +SpiceDB answers permissions questions by traversing a **tree** constructed from your [schema] (structure) and [relationships] (data). -A `CheckPermission` request will, for example, traverse starting from the resource+permission requested, -along any referenced permissions and relations, until the subject is found or maximum depth is -reached. +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. [schema]: /spicedb/concepts/schema [relationships]: /spicedb/concepts/relationships -## Max Depth +## How Traversal Works -In order to prevent requests from traversing without bounds, SpiceDB comes with a defaults to a depth of -`50`, after which computation is halted and an error is returned to the caller. +Consider this simple example: -This max depth is configurable via the `--dispatch-max-depth` flag. +``` +┌─────────────────────┐ +│ document:readme │ +│ permission: view │ +└─────────┬───────────┘ + │ viewer relation + ▼ +┌─────────────────────┐ +│ group:engineering │ +│ permission: member │ +└─────────┬───────────┘ + │ member relation + ▼ +┌─────────────────────┐ +│ user:alice │ +└─────────────────────┘ +``` + +When checking if `user:alice` can `view` `document:readme`, SpiceDB traverses: + +1. `document:readme#view` → follows `viewer` relation +2. `group:engineering#member` → follows `member` relation +3. Found `user:alice` → returns **allowed** + +Each arrow represents one "hop" in the traversal. +This tree has a **depth of 3** (three nodes visited). + +## Traversal Depth Limit + +To prevent unbounded traversal, SpiceDB enforces a maximum depth limit. +By default, this limit is **50 hops**. +If a traversal exceeds this limit, SpiceDB returns an error rather than continuing indefinitely. + +You can configure this limit with the `--dispatch-max-depth` flag: + +```sh +spicedb serve --dispatch-max-depth=100 +``` + +Most schemas work well within the default limit. +You typically only need to increase it if you have legitimately deep hierarchies (like deeply nested folder structures). -## Recursion in Relationships +## Cyclical Relationships (Cycles) -As a result of expecting the permissions graph to be a **tree**, SpiceDB _does not_ support recursive data dependencies that result in operations -(such as `CheckPermission`) visiting the _same_ object more than once. +A **cycle** occurs when traversing the permissions tree leads back to an object that was already visited. +SpiceDB does not support cyclical relationships because the permissions graph must be a [tree], not a graph with loops. -### Example +[tree]: https://zanzibar.tech/2SMVg4W_Wx:N:k -The following is an example of an **unsupported** nesting of groups: +### Example of a Cycle + +Consider this schema for nested groups: ```zed definition user {} @@ -39,83 +77,72 @@ definition resource { } ``` -and relationships: +With these relationships: ``` resource:someresource#viewer@group:firstgroup#member group:firstgroup#member@group:secondgroup#member group:secondgroup#member@group:thirdgroup#member -group:thirdgroup#member@group:firstgroup#member +group:thirdgroup#member@group:firstgroup#member ← creates a cycle! ``` -When computing a permission answer for `resource:someresource`, SpiceDB will attempt this walk: `resource:someresource#viewer` --> `group:firstgroup#member` -> `group:secondgroup#member` -> `group:thirdgroup#member` -> -`group:firstgroup#member` -> ..., causing a cycle. - -## Common Questions - -### Why doesn't SpiceDB simply support tracking the objects it has walked? - -1. Nested recursive "sets" have unclear semantics. - -2. Undesirable overhead. - -#### Nested sets have semantics issues +Visually, this creates a loop: -[Zanzibar] and ReBAC in general operate on _sets_: when a permission check is made, SpiceDB is -answering whether the requested subject is a member of the _set_ formed of all subjects that are -visited by walking the permissions tree. - -[Zanzibar]: https://zanzibar.tech - -The question becomes: if a group's members contains the members of _itself_, is that legal within -a set? -Much academic literature has been written about this topic (which we won't repeat here), -but the very question raises whether allowing such an approach is semantically valid. - -As a real example, imagine the following schema and relationships: +``` +┌──────────────────────┐ +│ resource:someresource│ +│ permission: view │ +└──────────┬───────────┘ + │ viewer + ▼ +┌──────────────────────┐ +│ group:firstgroup │◄─────────────────┐ +│ permission: member │ │ +└──────────┬───────────┘ │ + │ member │ + ▼ │ +┌──────────────────────┐ │ +│ group:secondgroup │ │ member +│ permission: member │ │ (cycle!) +└──────────┬───────────┘ │ + │ member │ + ▼ │ +┌──────────────────────┐ │ +│ group:thirdgroup │──────────────────┘ +│ permission: member │ +└──────────────────────┘ +``` -```zed -definition user {} +When SpiceDB traverses this, it walks: +`resource:someresource#viewer` → `group:firstgroup#member` → `group:secondgroup#member` → `group:thirdgroup#member` → `group:firstgroup#member` → ... -definition group { - relation direct_member: user | group#member - relation banned: user | group#member - permission member = direct_member - banned -} -``` +The traversal returns to `group:firstgroup#member`, creating an infinite loop. -``` -group:firstgroup#direct_member@group:secondgroup#member -group:firstgroup#banned@group:bannedgroup#member -group:secondgroup#direct_member@user:tom -group:bannedgroup#direct_member@group:firstgroup#member -``` +### How SpiceDB Handles Cycles -As we see above,`user:tom` is a `direct_member` of `secondgroup`, which makes him a member -of `firstgroup` -> which implies he's a member of `bannedgroup` -> which implies he's _not_ -a member of `firstgroup` -> thus making him no longer `banned` -> (logical inconsistency) +SpiceDB does not have a dedicated cycle detector. +Instead, when a cycle exists, the traversal continues looping until it hits the **maximum depth limit**, then returns an error. +This same error occurs whether the cause is a cycle or simply a very deep (but acyclic) hierarchy. -Thus, to prevent the above issue from occurring, Zanzibar and other ReBAC implementations such -as SpiceDB assume the permissions graph is a [tree]. +**Why not track visited objects?** +SpiceDB intentionally avoids tracking visited objects for two reasons: -[tree]: https://zanzibar.tech/2SMVg4W_Wx:N:k +1. **Semantic problems with self-referential sets**: When a group's members include itself, it creates logical paradoxes. + 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? -#### Overhead +2. **Performance overhead**: Tracking every visited object would require significant memory and network overhead, especially in distributed deployments. -From a practical perspective, tracking of visited objects when computing `CheckPermission` and -other permissions queries results in having significant amount of overhead over the wire and in -memory to track the full set of encountered objects and check for duplicates. +## Common Questions -### What do I do about a max depth error on CheckPermission? +### What do I do about a max depth error? -If you've received an error like: +If you see an error like: ``` 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 ``` -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: +Use `zed permission check` with `--explain` to visualize the traversal path: ```sh zed permission check resource:someresource view user:someuser --explain @@ -130,20 +157,28 @@ zed permission check resource:someresource view user:someuser --explain └── ! group:firstgroup member (cycle) (3.194125ms) ``` -### Why did my check work with recursion? +The output shows each hop in the traversal. +If you see `(cycle)` in the output, you have a cyclical relationship. +If there's no cycle, your hierarchy is simply deeper than the limit allows. + +### Why did my check succeed despite having a cycle? -SpiceDB automatically short-circuits `CheckPermission` operations when the target subject has been -found. +SpiceDB short-circuits `CheckPermission` when it finds the subject. +If the subject is found before the traversal hits the cycle or exceeds the depth limit, the check succeeds. -If the subject was found before the maximum depth was hit, then the operation will complete -successfully. -_However_, if the subject was not found, SpiceDB will continue walking, and ultimately return -the error you saw. +However, if the subject is **not** found, the traversal continues until it hits the depth limit and returns an error. -### How do I check for a possible recursion when writing a relationship? +### How do I prevent cycles when writing relationships? + +Before writing a relationship that could create a cycle, use `CheckPermission` to verify the relationship won't create a loop. + +For example, before writing `group:parent#member@group:child#member`, check if the parent is already reachable from the child: + +```sh +zed permission check group:child member group:parent +``` -Use the `CheckPermission` API to check if the subject contains the resource. +If this check returns **allowed**, writing the relationship would create a cycle. +If it returns **denied**, the relationship is safe to write. -For example, if writing the relationship `group:someparent#member@group:somechild#member` a check -can be made for `group:somechild#member@group:someparent#member`: if the _parent_ has permission -_on the child_, then the addition of this relationship will cause a recursion. +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. diff --git a/app/spicedb/modeling/validation-testing-debugging/page.mdx b/app/spicedb/modeling/validation-testing-debugging/page.mdx index 854dfacd..d914b7ce 100644 --- a/app/spicedb/modeling/validation-testing-debugging/page.mdx +++ b/app/spicedb/modeling/validation-testing-debugging/page.mdx @@ -243,7 +243,7 @@ true This command will also highlight which parts of the traversal were cached and if a [cycle] is detected. -[cycle]: ./recursion-and-max-depth#recursion-in-relationships +[cycle]: ./recursion-and-max-depth#cyclical-relationships-cycles [tracing header]: #checkpermission-tracing-header ## SpiceDB GitHub Actions