Authorization Models: RBAC, ABAC, and ReBAC
Authentication establishes who a user is; authorization decides what they may do — which actions on which resources, under which conditions. Every request to read an invoice, edit a document, or invite a teammate is an authorization decision, and getting a single one wrong can expose another customer's data. Broken access control has topped the OWASP Top 10 since 2021 for exactly that reason.
An authorization model is the vocabulary for expressing those decisions: roles, attributes, relationships, or a combination. The model you choose shapes how permissions are stored, how policies change as the product grows, and how easy it is to answer "who can see this?". Choosing well early saves painful migrations later; enforcing consistently, at every resource boundary, matters even more.
TL;DR
- An authorization decision combines a subject, an action, a resource, and context: can this user perform this action on this resource right now?
- RBAC grants permissions through roles — simple and auditable, but coarse; it breaks down when access depends on ownership or data attributes.
- ABAC evaluates attributes of the user, resource, and environment — flexible, but policies can become hard to reason about.
- ReBAC derives access from relationships (owner, member of, parent folder) — the natural fit for sharing and hierarchies, popularized by Google's Zanzibar.
- Enforce deny by default, check permissions on every object, and keep policy in one place rather than scattered
if user.is_adminchecks. - Test authorization as a matrix of roles, resources, and actions — especially across tenants.
Quick Example
A typical SaaS check combines three ideas: a tenant boundary that is never crossed, a role that grants the action, and a rule about the specific resource.
Code calls authorize(user, "invoice:update", invoice) and checks permissions, never role names — so roles can change without touching every endpoint.
Core Concepts
Anatomy of an Authorization Decision
Architectures often separate the policy enforcement point (the code that asks the question and blocks the request) from the policy decision point (the component that answers it), with policy and attribute stores feeding the decision.
Access Control Lists
An ACL attaches a list of who-can-do-what to each resource. ACLs are intuitive for individual sharing ("share this file with Dana") but hard to manage at scale and hard to answer reverse questions such as "what can Dana access?".
Role-Based Access Control (RBAC)
Users receive roles; roles carry permissions. RBAC is easy to explain, audit, and administer, which is why most products start with it.
Its weakness is granularity. When access depends on which resource — "editors can edit documents in their own department", "managers can approve expenses of their direct reports" — teams create ever more roles (editor-finance, editor-sales-emea) until role explosion makes the system unmanageable.
Attribute-Based Access Control (ABAC)
ABAC evaluates policies over attributes of the subject (department, clearance), the resource (owner, classification, status), the action, and the environment (time, location). It expresses fine-grained rules without new roles — "users may read documents whose department equals their own" — at the cost of policies that are harder to audit and debug.
Relationship-Based Access Control (ReBAC)
ReBAC derives permissions from a graph of relationships: a user is a viewer of a document if they own it, if it was shared with them, or if they are a member of the organization that owns the folder containing it. Google's Zanzibar paper described this model for products such as Drive, and open-source systems such as OpenFGA and SpiceDB implement it.
The OpenFGA model above says that organization admins can edit every document in their organization and members can view them, while still allowing direct sharing with individual users.
Comparing the Models
Real systems mix them: roles for broad capabilities, relationships for sharing and ownership, attributes for context such as MFA freshness or data classification.
Enforcing Authorization
Check at Every Resource Boundary
Authorization must run on the server for every request that touches a resource — including list and search endpoints, file downloads, exports, GraphQL resolvers, WebSocket messages, webhooks, and background jobs acting on a user's behalf. Hiding a button in the UI is not authorization.
Filter Lists, Not Just Single Objects
Fetching one object by ID is easy to protect; listing objects is where leaks hide. Push permission filters into queries — for example WHERE tenant_id = :tenant everywhere, or database-enforced row-level security — so unauthorized rows are never loaded.
Centralize Policy
Put decisions behind one function or service (authorize(subject, action, resource)) instead of scattering role checks across handlers. Policy engines make this explicit:
Log Decisions
Record denied requests and sensitive allowed actions (exports, permission changes, impersonation) in audit logs. Denials are also an attack signal: many denials from one user often mean someone is probing IDs.
Best Practices
Deny by Default
Every new endpoint and resource type should be inaccessible until a policy explicitly allows it. Forgetting an allow rule produces a bug report; forgetting a deny rule produces a breach.
Check Permissions, Not Roles, in Code
can(user, "invoice:update", invoice) survives role changes; if user.role == "admin" does not. Map roles to permissions in one place.
Keep the Tenant Boundary Separate
Treat tenant isolation as a non-negotiable check that runs before role logic, ideally backed by data-layer enforcement. See Multi-Tenant Application Design.
Keep Roles Few and Meaningful
A small number of well-named roles, plus resource-level relationships for sharing, avoids role explosion. Review unused and over-privileged roles periodically.
Test the Matrix
Generate tests from a table of roles, resources, and actions, including negative cases across tenants. See Authorization Testing Across Tenant Boundaries.
Common Mistakes
Checking Only in the UI
Missing Object-Level Checks (IDOR)
Role Explosion
FAQ
What is the difference between RBAC and ABAC?
RBAC grants permissions through roles assigned to users, so all users with a role get the same capabilities. ABAC evaluates rules over attributes of the user, resource, and environment, so access can depend on ownership, department, data classification, or time. Many systems use roles for broad capabilities and attributes for fine-grained conditions.
What is ReBAC and when should I use it?
Relationship-based access control derives permissions from relationships — owner, member, parent folder, shared with — stored as a graph. It is the natural choice for collaborative products with sharing, teams, and nested workspaces, such as document editors and project tools.
Should we build our own authorization or use a service?
Small applications can implement a central, well-tested permission module themselves. When permissions involve sharing, hierarchies, many services, or audit requirements, a policy engine (OPA, Cedar) or a Zanzibar-style service (OpenFGA, SpiceDB) saves considerable effort and reduces inconsistency.
How do I handle permissions in list and search endpoints?
Filter at the data layer so unauthorized records are never retrieved: scope every query by tenant, use row-level security where available, or ask the authorization system for the set of accessible IDs. Filtering after loading risks leaking data through counts, pagination, and errors.
Should a cross-tenant request return 403 or 404?
Usually 404. Returning 403 confirms that the resource exists, which helps attackers enumerate IDs. Within a tenant, 403 is appropriate when the user can know the resource exists but lacks permission for the action.
Related Topics
- OpenFGA — Relationship tuples, authorization models, consistency, schema migration and authorization testing.
- Business Logic Abuse and Fraud Controls — Model misuse of discounts, quotas, workflows, and account privileges and verify server-enforced invariants.
- Identity & Access Management — Identity, authentication, and access together
- Authentication — Establishing who the user is before authorizing
- Authorization Testing Across Tenant Boundaries — Proving the model is enforced
- Broken Access Control — The vulnerability class weak authorization creates
- Row-Level Security — Enforcing access rules inside the database
- Multi-Tenant Application Design — Tenant isolation as an authorization boundary
- Privileged Access Management — Controlling the most powerful permissions