Scaling Permissions with ReBAC: A Guide to OpenFGA and Zanzibar
Most developers start their authorization journey with Role-Based Access Control (RBAC). It is simple, intuitive, and works perfectly for small applications: users are assigned to roles like 'Admin' or 'Editor,' and those roles have specific permissions. However, as systems grow in complexity—especially in distributed, multi-tenant, or collaborative environments—RBAC quickly hits a wall.
When your product manager asks for features like 'allow users to share a specific folder with a specific group of people' or 'grant view access to anyone who is a member of the project’s parent organization,' the limitations of RBAC become glaringly obvious. You find yourself creating thousands of hyper-specific roles or embedding complex, hard-to-maintain logic directly into your SQL queries. This is where Relationship-Based Access Control (ReBAC) and the Zanzibar model come into play.
The Shift to Relationship-Based Access Control (ReBAC)
ReBAC is a paradigm shift. Instead of assigning permissions based on static roles, ReBAC determines access based on the relationships between entities. In a ReBAC system, we don't ask 'Does this user have the Editor role?' Instead, we ask 'What is the relationship between this user and this specific document?'
This approach is inspired by Google's Zanzibar, the global authorization system that powers Google Drive, YouTube, and Cloud IAM. Zanzibar proved that you could manage trillions of authorization rules with low latency and high availability by treating authorization as a graph problem.
OpenFGA (Fine-Grained Authorization) is an open-source implementation of the Zanzibar model, backed by the CNCF. It provides a developer-friendly way to implement ReBAC without needing to build the complex distributed infrastructure from scratch.
Understanding the Core Concepts: The Tuple
At the heart of the Zanzibar model and OpenFGA is the concept of a Relationship Tuple. A tuple is a simple assertion of a relationship between a subject and an object. It follows a basic structure:
user:anne is a viewer of document:roadmap_2024
In OpenFGA, this is represented as:
- Subject: The entity requesting access (e.g.,
user:anne). - Relation: The type of connection (e.g.,
viewer). - Object: The resource being accessed (e.g.,
document:roadmap_2024).
By storing these granular relationships, the authorization system can traverse the graph to determine access. If Anne is a member of the 'Marketing' group, and the 'Marketing' group has 'viewer' access to the 'Q4 Folder,' and the 'Q4 Folder' contains the 'Roadmap 2024' document, OpenFGA can infer that Anne has access through transitive relationships.
Modeling Permissions with OpenFGA DSL
One of the primary advantages of OpenFGA is its Domain Specific Language (DSL). Instead of writing complex code, you define your authorization model declaratively. Let's look at a real-world example: a repository management system like GitHub.
Defining the Model
model schema 1.1 type user type organization relations define admin: [user] define member: [user] type repository relations define parent_org: [organization] define owner: [user, organization#admin] define writer: [user, repository#owner] define reader: [user, repository#writer, organization#member]
In this model:
- A
usercan be anadminormemberof anorganization. - A
repositoryhas aparent_org. - The
ownerof a repository can be an individual user or the admins of the parent organization. - A
writerincludes anyone explicitly defined as a writer, plus anyone who is anowner(inheritance). - A
readerincludes writers, plus anyone who is amemberof the parent organization.
This declarative approach separates authorization logic from business logic. Your application code no longer needs to know why a user has access; it only needs to ask OpenFGA if they have access.
The Power of Transitive Relationships and Nesting
Traditional RBAC struggles with deep nesting. If you have a folder structure five levels deep, calculating permissions in a standard relational database often requires recursive Common Table Expressions (CTEs) or complex joins that don't scale.
ReBAC handles this naturally. Because OpenFGA treats permissions as a graph, it can efficiently resolve transitive relations. Consider this scenario:
user:bobis amemberofgroup:engineering.group:engineeringis avieweroffolder:product_specs.document:api_designhasfolder:product_specsas itsparent.
When Bob tries to read the api_design document, OpenFGA traverses these links. It sees that Bob is in the engineering group, which has access to the parent folder, and concludes that Bob has the viewer relation for the document. This "walk" through the graph happens in milliseconds, even with millions of tuples.
Architectural Integration: The 'Check' API
In a distributed system, you want your authorization service to be a centralized source of truth but highly performant. OpenFGA provides a simple Check API.
When a request hits your microservice, the flow looks like this:
- Authentication: The service identifies the user (e.g., via a JWT).
- Authorization Request: The service calls OpenFGA:
Check(user:bob, relation:reader, object:repository:123). - Decision: OpenFGA returns
allowed: trueorfalsebased on the current state of the relationship graph. - Enforcement: The service either proceeds with the request or returns a 403 Forbidden.
This decoupling is critical for microservices. You don't want every service to have its own local copy of the permissions database, which leads to synchronization nightmares and 'zombie' permissions.
Solving the 'New World' Problem
One of the hardest problems in distributed authorization is what the Zanzibar paper calls the 'New World' problem. This occurs when a user's permissions are revoked, but due to caching or database replication lag, they still have access for a few seconds (or minutes).
Zanzibar (and OpenFGA) addresses this through the use of 'Snapshots' or 'Continuation Tokens.' When you write a relationship change (e.g., removing a user from a group), the system provides a version token. When making a subsequent Check call, you can provide this token to ensure the authorization engine is looking at a state of the world that is at least as fresh as the change you just made. This ensures causal consistency across distributed nodes.
Implementation Strategy: Where to Start?
Transitioning to ReBAC isn't an all-or-nothing endeavor. Here is a recommended path for senior engineers looking to implement this:
1. Identify the 'Permission Leak'
Look for areas in your codebase where you are manually checking ownership or performing complex joins to verify access. These are your primary candidates for ReBAC.
2. Prototype the Model
Use the OpenFGA Playground. It allows you to visualize your model and test tuples against it in real-time. This is often where you'll discover that your permission logic is simpler (or more complex) than you initially thought.
3. Centralize the Tuples
Start syncing your core relationships to OpenFGA. When a user creates a project, write a tuple. When a user joins a team, write a tuple. Treat these as 'Facts' about your system.
4. Implement Middleware
Instead of sprinkling authorization checks throughout your business logic, implement them at the edge (API Gateway) or in a shared middleware layer. This ensures consistent enforcement across all your services.
Performance and Scalability Considerations
While ReBAC is powerful, it is not a silver bullet. You must consider the following:
- Latency: Every
Checkcall adds a network hop. OpenFGA is designed for speed, but you should still consider local caching of results for high-traffic endpoints, provided you have a strategy for cache invalidation. - Data Consistency: Ensure that your writes to your primary database and your writes to OpenFGA are coordinated. While they don't necessarily need to be in a single distributed transaction, they should be eventually consistent and resilient to failures (e.g., using an outbox pattern).
- Tuple Management: Be careful not to create an 'explosion' of tuples. If you can represent a permission via a broader relationship (like organization membership), do that instead of creating individual tuples for every single user-resource pair.
Conclusion
As systems evolve from simple monolithic applications to complex, interconnected ecosystems, the way we handle permissions must evolve too. RBAC is excellent for basic categorization, but it fails to capture the intricate web of relationships that define modern collaboration.
Implementing ReBAC with OpenFGA allows you to build a flexible, scalable, and maintainable authorization layer that stays out of your business logic. By treating authorization as a graph of relationships, you gain the ability to handle complex sharing models and deep hierarchies with the same performance and ease as a simple role check.
Actionable Next Steps:
- Audit your current authorization logic for 'RBAC bloat.'
- Explore the Zanzibar paper to understand the underlying theory of global-scale authorization.
- Spin up an OpenFGA instance via Docker and model one of your existing features using the DSL.
- Evaluate the impact of moving authorization checks to a centralized service on your system's latency and reliability.