DEV Community

sep83
sep83

Posted on

Beyond RBAC: Designing Scalable Access Control Without Role Explosion

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Is the user in the correct context or boundary?
  2. Does the role allow this action type?
  3. Is there an assignment for this resource?
  4. 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

  1. Roles should express capability, not scope
  2. Granularity belongs in data, not role names
  3. Conjunctive authorization improves safety and clarity
  4. Authorization rules should be auditable, not implicit
  5. Design access control with growth in mind

Good access control is not about more rules—it’s about clearer boundaries.

Top comments (0)