Zanzibar Relationship Checks
Zanzibar (from Google's 2019 paper of the same name; the model is also called ReBAC, relationship-based access control) models authorization as a graph of relation tuples. Each tuple has the form object#relation@subject — for example document:readme#editor@user:alice reads as "alice is an editor of document readme". A check then asks "does a path exist in the relation graph from this subject to this object under this relation, given the schema's rewrites?"
Use Zanzibar when access is driven by hierarchy and relationships: folders containing files, teams owning projects, organizations containing workspaces. Permission inheritance (folder editors can edit the folder's files) and shared access (all members of a team can view a document) are natural in this model and awkward in RBAC.
POST /orgs/{orgId}/api/v1/zanzibar/check
The canonical, organization-scoped route is POST /orgs/{orgId}/api/v1/zanzibar/check
and takes the subject in a field named user (used throughout this page).
Two related routes exist:
POST /api/v1/zanzibar/check— a deprecated tenantless alias. It still works and uses theuserfield, but prefer the organization-scoped route above.POST /api/v1/authz/zanzibar/check— an unscoped variant that names the subject fieldsubjectinstead ofuser. This is the field name the JavaScript and TypeScript SDKs use.
Zanzibar checks need the zanzibar feature, sold only as the Fine-grained authorization add-on ($99/month, available on Pro and Business) together with ABAC — no plan includes it. With billing enforced (BILLING_ENFORCEMENT=enforce, the production default) and a plan without it, every check and MCP per-tool grant returns allowed: false, and authoring namespaces, tuples or AI policies with relations is refused (403 feature_not_in_plan on JSON requests, an upgrade redirect in the portal). See Zanzibar in the user guide.
How Zanzibar Checks Work
RBAC asks "does this user have permission X?" — a flat yes/no against a role set. Zanzibar asks "is this user related to this object through this relation?" — and the engine walks the graph to decide.
During a check, LumoAuth:
- Looks up direct tuples that match
object#relation@subject. - Follows userset rewrites in the schema: for example, if
editorimpliesviewer, aneditortuple also satisfies aviewercheck. - Follows computed usersets across relations: for example, viewers of a parent folder become viewers of its documents.
- Returns
allowed: trueif any path is found, otherwisefalse.
A companion expand operation returns the full tree of subjects that satisfy a given object#relation, which is useful for audit and UI rendering.
Who May Ask About Whom
A relationship check names a subject explicitly, so the endpoint could be used as an oracle: "does some other user have access to this document?". LumoAuth therefore distinguishes two cases:
| Request | Requirement |
|---|---|
The subject is the caller itself (user:<own id>, or agent:<own id> for agent principals) | Any authenticated user or agent |
The subject is anyone else — another user, an agent, or a userset such as team:eng#member | The caller must hold the authz.check permission (granted to the default Tenant Admin role, assignable to any role) or present an access token / API key carrying the authz:check scope. |
A third-party check without that privilege is refused with HTTP 403:
{
"error": "insufficient_permissions",
"error_description": "Checking authorization for a subject other than yourself requires the \"authz.check\" permission or the \"authz:check\" scope",
"code": "INSUFFICIENT_PERMISSIONS"
}
The same rule applies to the RBAC check endpoints when they name a subject, to the AuthZEN evaluation/evaluations endpoints (the standard "may this subject do this action on this resource?" API; subject.id vs. the caller), and to expand, which always reveals other subjects and therefore always requires the privilege.
Request Format
| Field | Type | Required | Description |
|---|---|---|---|
object | string | Yes | Resource being accessed (namespace:id) |
relation | string | Yes | Type of access (e.g., viewer, editor, owner) |
user | string | Yes | Subject (namespace:id or namespace:id#relation userset) |
Basic Example
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/check \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"object": "document:annual-report",
"relation": "editor",
"user": "user:alice"
}'
{
"allowed": true,
"object": "document:annual-report",
"relation": "editor",
"user": "user:alice"
}
Understanding the Format
Objects
Objects are the resources in your system, named namespace:id.
| Example | Meaning |
|---|---|
document:readme | A document with ID "readme" |
folder:projects | A folder named "projects" |
organization:acme | Organization "acme" |
repository:myapp | A repository named "myapp" |
Relations
A relation is the kind of edge between an object and a subject. Relation names are defined per namespace in the schema.
| Relation | Typical Use | Usually Implies |
|---|---|---|
owner | Full control over a resource | editor, viewer |
editor | Can modify a resource | viewer |
viewer | Read-only access | - |
member | Belongs to a group or org | viewer (of group resources) |
admin | Administrative access | owner, member |
parent | Hierarchical containment | Permission inheritance |
Subjects
A subject is either a concrete identity or a userset — a reference to every entity holding a given relation on another object.
| Format | Example | Meaning |
|---|---|---|
namespace:id | user:alice | A specific user |
namespace:id#relation | team:engineering#member | All members of the engineering team |
namespace:id#relation | organization:acme#admin | All admins of the Acme org |
Userset Subjects
Userset subjects let you attach an entire group to an object in a single tuple. The check walks into the userset when evaluating.
Given tuples like team:engineering#member@user:bob and document:api-docs#viewer@team:engineering#member, the following check returns allowed: true:
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/check \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"object": "document:api-docs",
"relation": "viewer",
"user": "user:bob"
}'
The engine reasons:
- Bob is a
memberofteam:engineering. team:engineering#memberis aviewerofdocument:api-docs.- Therefore Bob is a
viewerofdocument:api-docs.
Examples
Document Sharing (Google Docs Style)
# Can Carol view the Q4 report?
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/check \
-d '{"object": "document:q4-report", "relation": "viewer", "user": "user:carol"}'
# Result: allowed: true (Carol is on finance team, which has viewer access)
# Can Carol edit the Q4 report?
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/check \
-d '{"object": "document:q4-report", "relation": "editor", "user": "user:carol"}'
# Result: allowed: false (Carol only has view access via team membership)
Folder Hierarchy
# Can Alice view the readme?
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/check \
-d '{"object": "document:readme", "relation": "viewer", "user": "user:alice"}'
# Result: allowed: true (Alice owns the parent folder)
GitHub-Style Repository Access
# Org structure
organization:acme#admin@user:ceo
organization:acme#member@user:developer
# Team in org
team:platform#parent@organization:acme
team:platform#member@user:developer
# Repo owned by team
repository:api-service#owner@team:platform#member
# Schema: org admins are admins of all repos
# Schema: team members who own a repo are maintainers
Expand: Who Has This Relation?
POST /orgs/{orgId}/api/v1/zanzibar/expand (also available unscoped as POST /api/v1/authz/zanzibar/expand)
Expand answers the inverse question of a check: instead of "is Alice a viewer?", it returns every subject that satisfies object#relation, as a tree that mirrors the namespace's rewrite rules. Use it for access audits ("who can see this document, and why?") and for sharing UIs.
Because the result always reveals other subjects, expand requires the authz.check permission or the authz:check scope — see Who May Ask About Whom.
Request Format
| Field | Type | Required | Description |
|---|---|---|---|
object | string | Yes | Resource to expand (namespace:id) |
relation | string | Yes | Relation to expand (e.g., viewer) |
Response Format
{
"tree": {
"type": "union",
"object": "document:123",
"relation": "viewer",
"children": [ ... ],
"subjects": []
}
}
Every node has the same five fields:
| Field | Type | Description |
|---|---|---|
type | "union" | "intersection" | "leaf" | How the node's children combine. Leaves have no children. |
object | string | The object this node was expanded for (namespace:id) |
relation | string | The relation this node was expanded for |
children | array | Nested nodes of the same shape (empty for leaves) |
subjects | array of strings | Direct subjects of a leaf: user:1, agent:agt_…, or usersets like group:2#member. Usersets are listed as-is and not recursed into. Empty on union/intersection nodes. |
How the schema maps to the tree:
| Rewrite rule | Resulting node |
|---|---|
_this | A leaf listing the stored tuples for object#relation |
computed_userset (e.g. viewer ← editor) | The expanded node of the same object under the referenced relation |
tuple_to_userset (e.g. viewer ← parent folder's viewer) | One expanded child per matching tuple, on the referenced object |
union / intersection | A node of that type with one child per branch. A definition that combines _this with a computed userset is rendered as a union. |
Expansion is capped at a depth of 10 and stops when it revisits an object#relation already on the current path (recursive schemas terminate; the revisited node is rendered as an empty leaf). Expanding an unknown object or relation returns an empty leaf, not an error.
Checks are bounded the same way: a check evaluates each object#relation@subject node at most once (cycles and duplicate tuples do not multiply work) and gives up with a deny after 2,000 nodes, which is logged. Duplicate relation tuples are refused when created in the portal.
Example
Given the schema viewer = _this ∪ editor and the tuples document:readme#viewer@user:bob, document:readme#viewer@team:eng#member and document:readme#editor@user:alice:
curl -X POST https://api.example.com/orgs/{orgId}/api/v1/zanzibar/expand \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"object": "document:readme",
"relation": "viewer"
}'
{
"tree": {
"type": "union",
"object": "document:readme",
"relation": "viewer",
"children": [
{
"type": "leaf",
"object": "document:readme",
"relation": "viewer",
"children": [],
"subjects": ["user:bob", "team:eng#member"]
},
{
"type": "leaf",
"object": "document:readme",
"relation": "editor",
"children": [],
"subjects": ["user:alice"]
}
],
"subjects": []
}
}
| Status | Meaning |
|---|---|
200 | Tree returned |
400 | object/relation missing, or object is not namespace:id |
403 | insufficient_permissions — caller lacks authz.check / authz:check; or the token belongs to another organization |
Comparison: Zanzibar vs RBAC
| Aspect | RBAC Check | Zanzibar Check |
|---|---|---|
| Question | Does user have permission? | Does user have relation to object? |
| Example | document.edit | document:123#editor@user:456 |
| Resource aware | No (global permission) | Yes (specific resource) |
| Inheritance | Role hierarchy only | Object + relation hierarchy |
| Best for | Admin features, global actions | Shared resources, hierarchies |
Related
- Authorization Overview — model comparison and when to choose Zanzibar.
- Permission Checks — RBAC-style check endpoints.