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

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

References