How to Prevent Role Explosion in RBAC (Without Switching to ABAC)
When role names start encoding regions, thresholds, and ownership, you have role explosion. Here are three techniques — resource-scoped permissions, role composition, and promoting data to attributes — to keep an RBAC model maintainable at scale.
Role explosion is the slow death of an RBAC system. It starts innocently: you add a manager role. Then finance needs a manager who can only see their own region, so you add manager-us-east. Then a spending cap, so manager-us-east-under-10k. Eighteen months later you have 1,400 roles, most assigned to exactly one person, and nobody can answer “what can this user actually do?” This post is about preventing that — while staying in RBAC.
What Actually Causes Role Explosion
Role explosion has a single root cause: encoding data into role names. A role should represent a job function (“support agent”), not a data slice (“support agent for enterprise accounts in EMEA”). Every time a region, a dollar threshold, or an ownership rule ends up in a role name, you have multiplied your role count by the cardinality of that dimension.
Fix 1: Resource-Scoped Permissions and Wildcards
Most role duplication comes from resource granularity. Instead of a role per resource instance, use permissions that match patterns. A wildcard permission collapses hundreds of explicit grants into one:
// Instead of one role per project...
// role: "editor-project-a", "editor-project-b", "editor-project-c" ...
// Use a single role with a wildcard permission:
const editorRole = {
id: 'editor',
permissions: [
{ resource: 'project:*', action: 'read', effect: 'allow' },
{ resource: 'project:*', action: 'update', effect: 'allow' },
{ resource: 'project:*', action: 'delete', effect: 'deny' }, // deny-wins guardrail
],
}One editor role now covers every project, and the explicit deny on delete holds even if another role grants it.
Fix 2: Role Composition
Assign multiple small, orthogonal roles to a subject instead of minting one combined role per permutation. A user who is both a billing viewer and a support agent gets two roles — not a new billing-viewer-support-agent role. The permutations live in the assignment, not the role catalog.
// Additive assignment — permissions are the union of all assigned roles.
await assignRoles(scopeId, userId, ['support-agent', 'billing-viewer'])Fix 3: Promote Data Dimensions to Attributes
When a dimension is truly dynamic — the user’s region must equal the resource’s region — it does not belong in a role at all. Promote it to an attribute-based condition layered on top of the role. This is the hybrid RBAC + ABAC model we cover in our RBAC vs. ABAC deep-dive.
function canApprove(req) {
// Coarse RBAC gate:
if (!hasPermission(req.subject.id, 'invoice', 'approve')) return false
// Fine-grained condition instead of a "manager-<region>" role:
return req.subject.region === req.resource.region
}A Rule of Thumb
- Job function → role.
- Resource pattern → wildcard permission.
- Combination of duties → multiple role assignments.
- Runtime data comparison → attribute condition.
How WardenAuth Helps
WardenAuth supports resource- and action-level permissions, wildcard matching (project:*, *:read), additive multi-role assignment, and deny-wins semantics out of the box — the exact primitives that keep a role catalog small. You model job functions once and let patterns and assignments handle the combinatorics.
Start free at wardenauthz.com/register and model your first scope in minutes.