Let's summarize when RBAC works well and when you should consider alternatives.
Where RBAC Excels
RBAC is the right choice when your permissions are:
| Characteristic | Example |
|---|---|
| Role-centric | "Admins can delete, viewers can read" |
| Coarse-grained | A few roles cover all use cases |
| Limited Attribute Awareness | Permissions depend on few or no resource attributes or context |
Where RBAC Struggles
RBAC breaks down when permissions depend on:
1. Attributes Other Than User Role
As soon as you need to create multiple permutations of permissions for different attributes (e.g., ownership, document status), RBAC starts to struggle. You end up creating very specific permissions like "document:update:own-unlocked-draft" to cover each combination which quickly becomes unmanageable.
2. Environmental Factors
With RBAC it is impossible to take into consideration environmental factors such as time of day, location, or other contextual information that may play a role in determining access. For example, a user may only be able to push code to production during certain hours or from specific IP addresses.
The Symptom: Permission Explosion
When RBAC is the wrong fit, you'll notice your permission list growing uncontrollably:
// Started simple
type Permission = "document:read" | "document:update" | "document:delete"
// Then requirements came in...
type Permission =
| "document:read:all"
| "document:read:own"
| "document:read:non-draft"
| "document:read:published"
| "document:update:all"
| "document:update:unlocked"
| "document:update:own"
| "document:update:own-unlocked"
| "document:update:own-unlocked-draft"
| "document:delete:all"
| "document:delete:own"
// ... and it keeps growing
The Hybrid Trap
When teams hit RBAC limits, they often create hybrids:
// "RBAC" that's actually policy functions
export function canUpdateDocument(user, document) {
if (user.role === "admin") return !document.isLocked
if (user.role === "editor") return !document.isLocked
if (user.role === "author") {
return (
document.creatorId === user.id &&
!document.isLocked &&
document.status === "draft"
)
}
return false
}
This works, but:
- You've abandoned the simplicity of RBAC
- Permission logic is scattered across helper functions
- The
can(user, permission)pattern is inconsistent - You're essentially hand-building ABAC without the structure
When to Graduate from RBAC
Consider moving beyond RBAC when you find yourself:
Creating permission names with multiple conditions
"document:update:own-unlocked-draft"is a red flag
Writing helper functions for every permission check
- If
canDoX(user, resource)is the norm, RBAC isn't helping
- If
Duplicating attribute checks across the codebase
- The same
document.isLockedcheck appearing everywhere
- The same
Needing different permissions for the same action based on environment
- Same "edit" action has different rules on weekday vs weekend
The Path Forward
In the next section, we'll implement ABAC and see how it elegantly handles the requirements that broke our RBAC system. ABAC is much more work to set up initially, but it scales better for complex, attribute-driven access control scenarios.