Role-Based Access Control (RBAC) is often the first authorization model developers reach for—and for good reason. It is simple, intuitive, and easy to explain. But as systems evolve, RBAC frequently becomes a bottleneck rather than a solution.
This article explores why traditional RBAC fails at scale, compares it with other common authorization approaches, and presents a generalized hybrid pattern that avoids role explosion while preserving clarity, flexibility, and auditability.
The goal is not to replace RBAC, but to use it correctly.
Traditional RBAC: Strengths and Limits
At its core, RBAC maps roles to permissions:
ADMIN → create, read, update, delete
EDITOR → create, read, update
VIEWER → read
This works well when:
- All users of a role behave the same way
- Permissions are uniform across resources
- The system is small or single-tenant
However, RBAC quietly assumes something that rarely remains true:
A role fully defines what a user can do everywhere.
Once this assumption breaks, problems appear.
The Role Explosion Problem
As soon as permissions depend on context, roles start multiplying:
- Editor for project A
- Editor for project B
- Reviewer who can approve but not edit
- Viewer who can export but not modify
Each variation becomes a new role. Over time:
- Roles lose semantic meaning
- Authorization becomes harder to reason about
- Changes require migrations instead of configuration
This phenomenon is known as role explosion, and it is the most common failure mode of RBAC.
Common Alternatives (And Why They Struggle)
RBAC with Custom Logic
A frequent workaround is adding conditional checks in code:
role allows action
AND resource belongs to user
AND state is valid
This approach works short-term, but:
- Authorization logic becomes scattered
- Rules are implicit and hard to audit
- Behavior depends on code paths, not data
Attribute-Based Access Control (ABAC)
ABAC evaluates policies based on attributes:
user.department == resource.department
AND user.level >= resource.required_level
AND action in allowed_actions
ABAC is powerful, but often:
- Hard to debug
- Hard to explain
- Easy to over-engineer
It shines in policy-heavy environments, but is excessive for many applications.
Access Control Lists (ACLs)
ACLs attach permissions directly to resources:
Resource X:
User A → read, edit
User B → read
They provide excellent granularity, but:
- Scale poorly with many users
- Make global permission reasoning difficult
- Lack a clear notion of user capability
A Balanced Pattern: Capability vs Scope
A more scalable approach separates capability from scope.
Core Principle
Access is granted only if both conditions are true:
Access = Role allows action
AND Assignment grants action on resource
This creates a conjunctive authorization model that is strict, flexible, and auditable.
Roles as Capability Ceilings
Roles answer one question only:
What types of actions can this user ever perform?
They are:
- Global
- Few in number
- Stable over time
Roles should not encode:
- Resource ownership
- Project membership
- Temporary responsibilities
They define a maximum authority, not actual access.
Assignments as the Granularity Layer
Fine-grained permissions are handled through explicit assignments:
- Bound to a specific user
- Bound to a specific resource
- Containing an explicit list of allowed actions
- Optionally enriched with metadata (who assigned it, status, expiration)
Assignments answer:
What is this user allowed to do here?
The same user can have different permissions on different resources—without changing roles.
Why This Pattern Scales
No Role Explosion
New requirements create new assignments, not new roles.
Built-in Separation of Duties
If a role cannot approve, no assignment can override that.
Clear Audit Trail
Every permission is explicit, inspectable, and traceable.
Simpler Mental Model
Roles define potential, assignments define reality.
A Generic Authorization Flow
A typical authorization check follows a predictable sequence:
- Is the user in the correct context or boundary?
- Does the role allow this action type?
- Is there an assignment for this resource?
- Does the assignment include the action?
Each step is independent, testable, and explainable.
When to Use This Pattern
This model is a good fit when:
- Permissions vary per resource
- Users collaborate on subsets of shared data
- Separation of duties matters
- Auditability is required
- You want to avoid hardcoding rules
It may be unnecessary when:
- The system is small or single-tenant
- All users of a role behave identically
- Only a few permission levels exist
Key Takeaways
- Roles should express capability, not scope
- Granularity belongs in data, not role names
- Conjunctive authorization improves safety and clarity
- Authorization rules should be auditable, not implicit
- Design access control with growth in mind
Good access control is not about more rules—it’s about clearer boundaries.
Top comments (0)