Fine-Grained Authorization (Zanzibar Model)
Role-Based Access Control (RBAC) is sufficient for many applications: users have roles, roles have permissions, you check if a user has the right permission. But RBAC breaks down when you need resource-level access control — “Alice can edit document 42, but not document 43,” or “Bob can view all documents owned by his team.”
Auris implements a Zanzibar-compatible Fine-Grained Authorization (FGA) engine to handle these scenarios.
What is Zanzibar?
Zanzibar is Google’s global authorization system, published in a research paper in 2019. It powers access control for Gmail, Drive, Docs, Maps, and dozens of other Google products — handling trillions of authorization checks per second with millisecond latency.
The core insight of Zanzibar is modeling authorization as relationships between objects and subjects, stored as tuples. Authorization questions become graph traversal problems: “Is there a path from subject user:alice to object document:readme via the viewer relation?”
Auris implements a Zanzibar-compatible FGA engine with:
- An OpenFGA-compatible DSL for defining authorization models
- A tuple store backed by PostgreSQL (via Prisma)
- A recursive check algorithm with cycle detection
- Expand and list-objects query operations
- A visual debugger in the Auris Console
Core Concepts
Object
An object is any resource you want to protect. Objects have a type and an ID:
document:readme
folder:engineering
organization:acme-corp
report:q4-2024The type is defined in your authorization model. The ID is any string that identifies the specific instance.
Relation
A relation is a named edge in the authorization graph — a kind of relationship an object can have with subjects. Relations are defined per object type in the model:
documentcan have relations:owner,editor,viewerfoldercan have relations:owner,viewerorganizationcan have relations:admin,member
Subject
A subject is who (or what) has a relation to an object. Subjects can be:
- A user:
user:alice - A user set (another object’s relation):
group:engineering#member(all members of the engineering group)
Tuple
A tuple is a stored fact: “this subject has this relation to this object.”
Format: object#relation@subject
Examples:
document:readme#viewer@user:alice
document:readme#editor@user:bob
document:readme#viewer@group:engineering#member
folder:engineering#owner@user:charlieThe second-to-last example means: “all members of the engineering group are viewers of document readme.” This is called a userset — a group of subjects defined by another relation.
The Auris DSL
Auris uses an OpenFGA-compatible domain-specific language to define authorization models. The DSL specifies object types and their relations, including how relations are derived from other relations.
Basic Structure
type user
type group
relations
define member: [user]
type folder
relations
define owner: [user]
define viewer: [user, group#member] or owner
type document
relations
define parent: [folder]
define owner: [user] or owner from parent
define editor: [user, group#member] or owner
define viewer: [user, group#member] or editorEach type block defines an object type. relations lists the named relations for that type, with rules for how they are derived.
Reading Relation Definitions
define viewer: [user, group#member] or editorThis means: “A subject is a viewer of a document if:
- The subject is a
usertype AND there is a direct tupledocument:X#viewer@user:Y, OR - The subject is a
group#member(a member of some group) AND there is a direct tupledocument:X#viewer@group:Z, OR - The subject is an
editorof the document (computed recursively).”
Six Rewrite Types
The authorization engine supports six types of relation rewrite rules. They can be composed to model virtually any access pattern.
1. this — Direct Assignment
The simplest rule: the relation holds if a tuple exists directly in the store.
define owner: [user]“User Alice is an owner if the tuple document:X#owner@user:alice exists.”
2. computedUserset — Inherit from Another Relation
The relation holds if the subject has a different relation to the same object.
define editor: [user] or owner“User Alice is an editor if a direct editor tuple exists for her, OR if she is an owner (computed from the owner relation of the same document).“
3. tupleToUserset — Follow a Relation to Another Object
This is the most powerful rule. It follows a relation from the current object to another object, then checks a relation on that other object.
define owner: [user] or owner from parent“User Alice is an owner of document:X if a direct owner tuple exists for her, OR if there is a tuple document:X#parent@folder:Y AND Alice is an owner of folder:Y.”
This is how inheritance works in Google Drive: documents inherit permissions from their parent folder.
4. union — OR Combination
A union of two or more rewrites. The relation holds if any of the sub-rules match.
define viewer: [user] or editorThis is equivalent to an explicit union:
define viewer: {
union: {
child: [{ this: {} }, { computedUserset: { relation: "editor" } }]
}
}5. intersection — AND Combination
All sub-rules must hold for the relation to be true. Useful for requiring multiple conditions simultaneously.
define restricted_viewer: [user] and approved“A user is a restricted_viewer only if they have an explicit restricted_viewer tuple AND the document has an approved relation for them.”
6. exclusion — Set Difference (A BUT NOT B)
The relation holds for subjects in the first set but not in the second. Useful for deny lists.
define viewer: [user] but not blocked“A user can view if they have a viewer tuple, UNLESS they also have a blocked tuple.”
The Check Algorithm
When you call POST /api/fga/check, the engine evaluates whether objectType:objectId#relation@subjectType:subjectId holds.
High-Level Algorithm
check(object, relation, user):
model = getActiveModel()
ruleset = model.typeDefinitions[object.type].relations[relation]
return evaluate(ruleset, object, user, visited={})
evaluate(rewrite, object, user, visited):
if (object, rewrite) in visited:
return false // cycle detected
visited.add((object, rewrite))
switch rewrite.type:
case "this":
return tupleExists(object, relation, user)
case "computedUserset":
return evaluate(model.relations[rewrite.relation], object, user, visited)
case "tupleToUserset":
intermediateObjects = listTuples(object, rewrite.tupleset)
return any(evaluate(model[intermediateObject.type][rewrite.relation], intermediateObject, user, visited)
for intermediateObject in intermediateObjects)
case "union":
return any(evaluate(child, object, user, visited) for child in rewrite.children)
case "intersection":
return all(evaluate(child, object, user, visited) for child in rewrite.children)
case "exclusion":
return evaluate(rewrite.base, object, user, visited)
and not evaluate(rewrite.subtract, object, user, visited)Cycle Detection
The visited set prevents infinite loops in models with circular relation references. Max recursion depth is capped at 25.
Explain Mode
When explain: true is passed to POST /api/fga/check, the engine returns a resolution tree showing exactly which rule matched (or failed), down to the specific tuple that was found (or not found). This is invaluable for debugging access control issues.
Modeling Patterns
Google Drive Model
Users and groups have access to documents either directly or by inheriting from a parent folder:
type user
type group
relations
define member: [user]
type folder
relations
define owner: [user]
define editor: [user, group#member] or owner
define viewer: [user, group#member] or editor
type document
relations
define parent: [folder]
define owner: [user] or owner from parent
define editor: [user, group#member] or owner or editor from parent
define viewer: [user, group#member] or editor or viewer from parentExample tuples:
folder:engineering#owner@user:charlie
document:readme#parent@folder:engineering
document:readme#viewer@user:aliceResult: Alice can view the readme (direct tuple). Charlie can own, edit, and view the readme (inherited via folder ownership). Any member of a group that is an editor of the engineering folder can edit the readme (inherited via folder).
SaaS Multi-Tenant Model
type user
type organization
relations
define admin: [user]
define member: [user] or admin
type project
relations
define parent_org: [organization]
define owner: [user] or admin from parent_org
define contributor: [user, organization#member] or owner
define viewer: [user, organization#member] or contributorExample tuples:
organization:acme#admin@user:alice
project:alpha#parent_org@organization:acme
project:alpha#owner@user:bobResult: Alice is an admin of acme, therefore an owner of all acme projects (via admin from parent_org). All acme members can view project alpha (via organization#member).
GitHub-Style Repository Access
type user
type team
relations
define member: [user]
type organization
relations
define admin: [user]
define member: [user] or admin
type repository
relations
define parent_org: [organization]
define admin: [user, team#member] or admin from parent_org
define writer: [user, team#member] or admin
define reader: [user, team#member] or writer or member from parent_orgMigrating from RBAC to FGA
You do not have to choose one or the other. Auris supports using RBAC and FGA together:
RBAC handles: Coarse-grained feature access (view:invoices, manage:users)
FGA handles: Fine-grained resource-level access (document:X#editor@user:Y)
Migration Steps
-
Keep RBAC for feature gates: Continue using
POST /api/roles/checkfor permission checks likecreate:reportsthat apply globally. -
Add FGA for resource-level checks: When a user creates a document, write a tuple
document:X#owner@user:Y. Check this tuple when the user tries to edit the document. -
Enable the FGA engine: Set
FGA_ENGINE_ENABLED=truein your Auris deployment environment variables. -
Create an authorization model: Define your object types and relations in the Auris Console under Administration → Fine-Grained Authorization → Models.
-
Activate the model: Click “Activate” on your model — this makes it the live model for all check/expand/list-objects operations.
-
Write tuples: As objects are created and shared in your application, write tuples to the FGA store via
POST /api/fga/tuplesorPOST /api/fga/tuples/bulk. -
Integrate checks: In your resource server, call
POST /api/fga/checkbefore allowing operations on specific resources.
The Auris bridge layer (resource-access.ts in the Auris API) automatically routes authorization checks to the FGA engine when FGA_ENGINE_ENABLED=true, falling back to the legacy Keto (ReBAC) layer when false. This allows gradual migration without breaking existing authorization logic.
Debugging with the Console
The Auris Console provides a visual FGA debugger at Administration → Fine-Grained Authorization → Debugger. It supports three operations:
Check: Enter an object, relation, and subject — see whether the check passes or fails, with the full resolution tree showing every rule evaluated.
Expand: Enter an object and relation — see all subjects who have that relation, following the rewrite rules recursively.
List Objects: Enter a subject and relation — see all objects of a given type that the subject can access.
The visual resolution tree uses color coding:
- Green nodes: rule evaluated to true
- Red nodes: rule evaluated to false
- Blue nodes: intermediate computed usersets
This makes it straightforward to identify why a user does or does not have access to a specific resource.
Related Concepts
- Fine-Grained Authorization Guide — Practical guide to implementing FGA
- Roles & Permissions — How RBAC and FGA work together
- FGA Debugger — Test authorization checks in the Console
- Fine-Grained Authorization API — Models, tuples, check, expand, and list-objects endpoints
- JavaScript SDK —
client.fga.check()and tuple management