Skip to main content

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

Endpoint variants

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 the user field, but prefer the organization-scoped route above.
  • POST /api/v1/authz/zanzibar/check — an unscoped variant that names the subject field subject instead of user. This is the field name the JavaScript and TypeScript SDKs use.
Fine-grained authorization add-on

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:

  1. Looks up direct tuples that match object#relation@subject.
  2. Follows userset rewrites in the schema: for example, if editor implies viewer, an editor tuple also satisfies a viewer check.
  3. Follows computed usersets across relations: for example, viewers of a parent folder become viewers of its documents.
  4. Returns allowed: true if any path is found, otherwise false.

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:

RequestRequirement
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#memberThe 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​

FieldTypeRequiredDescription
objectstringYesResource being accessed (namespace:id)
relationstringYesType of access (e.g., viewer, editor, owner)
userstringYesSubject (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.

ExampleMeaning
document:readmeA document with ID "readme"
folder:projectsA folder named "projects"
organization:acmeOrganization "acme"
repository:myappA 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.

RelationTypical UseUsually Implies
ownerFull control over a resourceeditor, viewer
editorCan modify a resourceviewer
viewerRead-only access-
memberBelongs to a group or orgviewer (of group resources)
adminAdministrative accessowner, member
parentHierarchical containmentPermission inheritance

Subjects​

A subject is either a concrete identity or a userset — a reference to every entity holding a given relation on another object.

FormatExampleMeaning
namespace:iduser:aliceA specific user
namespace:id#relationteam:engineering#memberAll members of the engineering team
namespace:id#relationorganization:acme#adminAll 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:

  1. Bob is a member of team:engineering.
  2. team:engineering#member is a viewer of document:api-docs.
  3. Therefore Bob is a viewer of document: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​

FieldTypeRequiredDescription
objectstringYesResource to expand (namespace:id)
relationstringYesRelation to expand (e.g., viewer)

Response Format​

{
"tree": {
"type": "union",
"object": "document:123",
"relation": "viewer",
"children": [ ... ],
"subjects": []
}
}

Every node has the same five fields:

FieldTypeDescription
type"union" | "intersection" | "leaf"How the node's children combine. Leaves have no children.
objectstringThe object this node was expanded for (namespace:id)
relationstringThe relation this node was expanded for
childrenarrayNested nodes of the same shape (empty for leaves)
subjectsarray of stringsDirect 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 ruleResulting node
_thisA 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 / intersectionA 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": []
}
}
StatusMeaning
200Tree returned
400object/relation missing, or object is not namespace:id
403insufficient_permissions — caller lacks authz.check / authz:check; or the token belongs to another organization

Comparison: Zanzibar vs RBAC​

AspectRBAC CheckZanzibar Check
QuestionDoes user have permission?Does user have relation to object?
Exampledocument.editdocument:123#editor@user:456
Resource awareNo (global permission)Yes (specific resource)
InheritanceRole hierarchy onlyObject + relation hierarchy
Best forAdmin features, global actionsShared resources, hierarchies