Skip to Content
ConceptsFGA / Zanzibar Model

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-2024

The 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:

  • document can have relations: owner, editor, viewer
  • folder can have relations: owner, viewer
  • organization can 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:charlie

The 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 editor

Each 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 editor

This means: “A subject is a viewer of a document if:

  • The subject is a user type AND there is a direct tuple document:X#viewer@user:Y, OR
  • The subject is a group#member (a member of some group) AND there is a direct tuple document:X#viewer@group:Z, OR
  • The subject is an editor of 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 editor

This 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 parent

Example tuples:

folder:engineering#owner@user:charlie document:readme#parent@folder:engineering document:readme#viewer@user:alice

Result: 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 contributor

Example tuples:

organization:acme#admin@user:alice project:alpha#parent_org@organization:acme project:alpha#owner@user:bob

Result: 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_org

Migrating 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

  1. Keep RBAC for feature gates: Continue using POST /api/roles/check for permission checks like create:reports that apply globally.

  2. 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.

  3. Enable the FGA engine: Set FGA_ENGINE_ENABLED=true in your Auris deployment environment variables.

  4. Create an authorization model: Define your object types and relations in the Auris Console under Administration → Fine-Grained Authorization → Models.

  5. Activate the model: Click “Activate” on your model — this makes it the live model for all check/expand/list-objects operations.

  6. Write tuples: As objects are created and shared in your application, write tuples to the FGA store via POST /api/fga/tuples or POST /api/fga/tuples/bulk.

  7. Integrate checks: In your resource server, call POST /api/fga/check before 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.