RBAC/ABAC
RBAC and ABAC
Understand how role-based and attribute-based access control model permissions, where each breaks down, and how to design an authorization layer that stays correct as the system grows.
Prerequisites
Recommended Knowledge
- Authentication versus authorization
- Identity claims and token validation
- OAuth 2.0 scopes and their limits
- Sessions and identity propagation
- Relational data modeling and joins
- Caching and cache invalidation
- API design and error semantics
- Trust boundaries from threat modeling
- Audit logging fundamentals
The Authorization Question
Authentication establishes who is making a request. Authorization decides whether that identity may perform a specific action on a specific resource. These are separate decisions, and conflating them is a persistent source of vulnerabilities.
Every authorization model is ultimately a way of answering that question consistently, at scale, and in a form that humans can reason about and auditors can verify.
Role-Based Access Control
RBAC assigns permissions to roles and roles to users. Rather than granting capabilities to individuals, the system groups related permissions into a named role that reflects a job function.
Simple Analogy
An employee badge grants access to the areas associated with their department. The badge does not list every door individually; it identifies a role, and the building's rules determine which doors that role opens.
Data Model
CREATE TABLE roles (
role_id BIGINT PRIMARY KEY,
role_name VARCHAR(80) NOT NULL UNIQUE,
description TEXT NULL
);
CREATE TABLE permissions (
permission_id BIGINT PRIMARY KEY,
permission_name VARCHAR(120) NOT NULL UNIQUE,
resource_type VARCHAR(80) NOT NULL,
action VARCHAR(40) NOT NULL
);
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
scope_type VARCHAR(40) NULL,
scope_id VARCHAR(80) NULL,
granted_at TIMESTAMP NOT NULL,
granted_by BIGINT NOT NULL,
expires_at TIMESTAMP NULL,
PRIMARY KEY (user_id, role_id, scope_type, scope_id)
);
The scope columns matter more than they first appear. Without them, a role applies globally, which is rarely what a multi-tenant or project-based system actually needs.
SELECT DISTINCT p.permission_name
FROM user_roles ur
JOIN role_permissions rp
ON rp.role_id = ur.role_id
JOIN permissions p
ON p.permission_id = rp.permission_id
WHERE ur.user_id = :user_id
AND (ur.expires_at IS NULL OR ur.expires_at > CURRENT_TIMESTAMP)
AND (ur.scope_type IS NULL
OR (ur.scope_type = :scope_type AND ur.scope_id = :scope_id));
Role Hierarchies
Roles frequently inherit from one another, so a senior role receives everything a junior role has plus additional permissions.
| Role | Inherits From | Additional Permissions |
|---|---|---|
| Viewer | None | Read published content |
| Contributor | Viewer | Create and edit own drafts |
| Editor | Contributor | Publish and edit any content |
| Administrator | Editor | Manage users, roles, and settings |
Role Explosion
RBAC degrades when permissions must vary by dimensions that roles cannot express. Teams respond by creating increasingly specific roles, and the count grows multiplicatively.
If roles must vary across region, department, and clearance level, the number of distinct roles becomes the product of those dimensions rather than their sum. Four regions, five departments, and three clearance levels already produce sixty roles for what is conceptually one job function.
Attribute-Based Access Control
ABAC evaluates policies against attributes of the subject, the resource, the action, and the environment. Instead of asking which role the user holds, it asks whether the current facts satisfy a written rule.
| Attribute Category | Examples | Typical Source |
|---|---|---|
| Subject | Department, clearance, employment status, manager | Identity provider and HR directory |
| Resource | Owner, classification, project, region, state | Resource metadata in the datastore |
| Action | Read, update, approve, export, delete | The API operation being invoked |
| Environment | Time, network, device posture, authentication strength | Request context and session metadata |
Example Policy
{
"policyId": "POL-0042",
"description": "Finance staff may read regional reports during business hours from managed devices.",
"effect": "Permit",
"target": {
"action": "report.read",
"resourceType": "financial-report"
},
"conditions": [
{ "attribute": "subject.department", "operator": "equals", "value": "finance" },
{ "attribute": "subject.employmentStatus", "operator": "equals", "value": "active" },
{ "attribute": "resource.region", "operator": "equalsAttribute", "value": "subject.region" },
{ "attribute": "resource.classification", "operator": "in", "value": ["internal", "confidential"] },
{ "attribute": "environment.deviceManaged", "operator": "equals", "value": true },
{ "attribute": "environment.authLevel", "operator": "equals", "value": "mfa" }
]
}
A single policy replaces what RBAC would express as one role per region. The varying dimension became a comparison between a subject attribute and a resource attribute.
RBAC Compared to ABAC
| Dimension | RBAC | ABAC |
|---|---|---|
| Decision basis | Assigned roles | Evaluated attributes |
| Granularity | Coarse, at the operation level | Fine, down to individual records |
| Context awareness | Limited or absent | Native, including time and device |
| Comprehensibility | High, roles map to job functions | Lower, requires reading policies |
| Scaling behavior | Role count multiplies | Policy count grows modestly |
| Evaluation cost | Low, a membership check | Higher, attributes must be gathered |
| Auditability | Straightforward to list role holders | Harder to enumerate who has access |
| Change management | Grant or revoke a role | Update attributes or policy logic |
| Best fit | Stable organizational structures | Contextual and data-dependent rules |
RBAC Strengths
- Easy for non-engineers to understand
- Simple to review during access certification
- Fast to evaluate and cache
- Maps cleanly to organizational charts
- Well supported by most frameworks
ABAC Strengths
- Expresses record-level conditions naturally
- Avoids multiplicative role growth
- Incorporates time, device, and network context
- Supports data-residency and classification rules
- Policies can be versioned and tested
Relationship-Based Access Control
A third model, ReBAC, decides access from relationships between subjects and resources. It suits systems where permission flows through containment or sharing, such as documents inside folders inside workspaces.
{
"tuples": [
{ "subject": "user:4821", "relation": "owner", "object": "document:9f2b" },
{ "subject": "user:5137", "relation": "editor", "object": "folder:project-x" },
{ "subject": "folder:project-x", "relation": "parent", "object": "document:9f2b" },
{ "subject": "group:finance", "relation": "viewer", "object": "folder:project-x" },
{ "subject": "user:6042", "relation": "member", "object": "group:finance" }
]
}
| Model | Core Question | Natural Use Case |
|---|---|---|
| RBAC | What role does this user hold? | Administrative consoles and internal tools |
| ABAC | Do the current attributes satisfy a policy? | Classification, residency, and contextual rules |
| ReBAC | Is this user connected to the resource? | Document sharing and hierarchical collaboration |
Authorization Architecture
Separating the decision point from the enforcement point keeps policy logic out of scattered application code.
Policy Enforcement Point
Where the decision is applied.
Sits in the request path, intercepts the operation, asks for a decision, and either permits the call or returns a denial.
Policy Decision Point
Where the decision is made.
Evaluates the applicable rules against the supplied context and returns a permit or deny outcome, ideally with a reason.
Policy Information Point
Where attributes come from.
Supplies subject, resource, and environment attributes from directories, databases, and request context.
Policy Administration Point
Where rules are authored and versioned.
Manages policy lifecycle, including review, approval, deployment, and rollback.
Where to Enforce
| Layer | What It Can Decide | Limitation |
|---|---|---|
| Client interface | Which controls to display | Never a security boundary, only usability |
| API gateway | Coarse route and scope checks | Lacks knowledge of specific records |
| Service layer | Operation and resource-level rules | Must be applied on every path consistently |
| Data layer | Row and column filtering | Harder to express complex business conditions |
| Derived stores | Search and analytics visibility | Frequently omitted, causing disclosure |
Implementing the Check
async function authorize(context) {
const { subject, action, resource, environment } = context;
const decision = await policyEngine.evaluate({
subject: {
id: subject.id,
roles: subject.roles,
department: subject.department,
region: subject.region,
clearance: subject.clearance
},
action: action,
resource: {
id: resource.id,
type: resource.type,
ownerId: resource.ownerId,
region: resource.region,
classification: resource.classification
},
environment: {
authLevel: environment.authLevel,
deviceManaged: environment.deviceManaged,
requestTime: environment.requestTime
}
});
await auditLog.record({
subjectId: subject.id,
action: action,
resourceId: resource.id,
decision: decision.effect,
matchedPolicy: decision.policyId,
reason: decision.reason,
occurredAt: new Date().toISOString()
});
return decision;
}
Recording the matched policy alongside the outcome is what makes an authorization system explainable later. Without it, incident investigation becomes guesswork.
Applying It in an Endpoint
async function getFinancialReport(request, response) {
const report = await reportRepository.findById(
request.params.reportId
);
if (!report) {
return response.status(404).json({ error: "not_found" });
}
const decision = await authorize({
subject: request.principal,
action: "report.read",
resource: report,
environment: request.securityContext
});
if (decision.effect !== "Permit") {
return response.status(404).json({ error: "not_found" });
}
return response.json(report);
}
Authorization in List Queries
Checking each record after retrieval breaks pagination and wastes work. The filter must be pushed into the query so that counts and page boundaries remain correct.
SELECT r.report_id,
r.title,
r.region,
r.classification,
r.created_at
FROM financial_reports r
WHERE r.region = :subject_region
AND r.classification IN ('internal', 'confidential')
AND (
r.owner_id = :subject_id
OR EXISTS (
SELECT 1
FROM report_shares s
WHERE s.report_id = r.report_id
AND s.principal_id = :subject_id
)
)
ORDER BY r.created_at DESC
LIMIT :page_size OFFSET :page_offset;
The same predicates must be applied to search indexes and analytics views. A derived store that omits them will leak records that the primary database correctly protects.
Performance and Caching
Authorization runs on every request, so its cost is multiplied across the entire system. Attribute gathering, not policy evaluation, is usually the expensive part.
| Cacheable Item | Typical Volatility | Invalidation Trigger |
|---|---|---|
| Role assignments | Low | Role grant or revocation |
| Permission sets per role | Very low | Role definition change |
| Subject attributes | Low to moderate | Directory synchronization |
| Compiled policies | Low | Policy deployment |
| Resource attributes | High | Record update, often not cached |
| Final decisions | High | Risky to cache, prefer short TTL |
Combining Rules
When multiple policies apply, the system needs a defined rule for resolving them.
| Algorithm | Behavior | When Appropriate |
|---|---|---|
| Deny overrides | Any deny defeats all permits | Default for sensitive systems |
| Permit overrides | Any permit defeats denials | Collaborative sharing scenarios |
| First applicable | The first matching policy decides | Ordered rule sets with clear precedence |
| Deny by default | Absence of a permit means denial | Always, as the baseline posture |
Least Privilege and Separation of Duties
Least Privilege
Grant only the permissions required for the current task, and remove them when the need ends. Standing broad access is the most common finding in access reviews.
Separation of Duties
Ensure that a single identity cannot both initiate and approve a sensitive operation, such as creating a payment and authorizing it.
Time-Bound Elevation
Grant elevated permissions temporarily with automatic expiry, rather than permanently assigning administrative roles.
Periodic Recertification
Require owners to review and reconfirm assignments on a schedule, so accumulated access is actively removed rather than silently retained.
SELECT ur.user_id,
r.role_name,
ur.granted_at,
ur.expires_at
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
WHERE r.role_name IN ('administrator', 'security-admin')
AND ur.expires_at IS NULL
ORDER BY ur.granted_at;
Queries like this surface permanent privileged grants, which are usually the highest-value target in an environment.
Threats and Mitigations
| Threat | Description | Mitigation |
|---|---|---|
| Broken object-level authorization | Identifier changed to access another user's record | Check permission on the specific resource, every time |
| Broken function-level authorization | Privileged endpoint reachable by ordinary users | Enforce on the server, not through interface hiding |
| Privilege creep | Permissions accumulate across role changes | Recertification and time-bound grants |
| Confused deputy | A service acts with its own broad privileges | Propagate the caller's identity and evaluate against it |
| Derived store leakage | Search index omits permission filters | Index access metadata and filter at query time |
| Stale cached decision | Revoked access still succeeds | Short TTLs and event-driven invalidation |
| Fail-open behavior | Engine unavailable, request allowed | Deny on evaluation failure and alert |
| Mass assignment | Client sets a role field during an update | Allow-list writable fields explicitly |
| Tenant boundary bypass | Cross-tenant record accessed | Enforce tenant scoping in every query |
Monitoring and Auditing
Signals Worth Tracking
- Denial rate by endpoint and by subject
- Repeated denials from one identity
- Privileged role grants and revocations
- Policy deployments and who approved them
- Policy evaluation latency and error rate
- Decisions made from cached attributes
- Access to highly classified resources
- Cross-tenant access attempts
- Permanent privileged assignments over time
- Recertification completion rates
Choosing a Model
Common Design Mistakes
Weak Design
- Checking roles but not the specific resource
- Encoding region or team into role names
- Scattering authorization logic across controllers
- Trusting role claims without verification
- Filtering results after retrieval
- Allowing the engine to fail open
- Omitting permission data from search indexes
- Granting permanent administrative access
- Logging the outcome without the reason
Strong Design
- Evaluates subject, action, and resource together
- Keeps varying dimensions in attributes
- Centralizes decisions behind one interface
- Resolves permissions server-side
- Pushes filters into the query
- Denies on any evaluation failure
- Propagates permissions to derived stores
- Uses time-bound elevation
- Records the matched policy in the audit trail
System Design Interview Discussion
| Question | What Your Answer Should Cover |
|---|---|
| RBAC or ABAC? | Whether permissions vary by data or context |
| Where is the check performed? | Server-side enforcement and its placement |
| How do list queries stay correct? | Predicate pushdown rather than post-filtering |
| How is search protected? | Indexed access metadata and query-time filtering |
| What is the per-request cost? | Attribute gathering, caching, and invalidation |
| What happens on engine failure? | Fail-closed behavior and alerting |
| How is a permission revoked? | Propagation path and cache invalidation delay |
| Who can access a given resource? | Whether the model supports reverse queries |
Implementation Checklist
Production Checklist
- Deny by default on every path
- Check the specific resource, not only the operation
- Centralize decisions behind a single interface
- Resolve roles and attributes server-side
- Scope role assignments to tenant or project
- Keep role hierarchies shallow and traceable
- Push authorization predicates into queries
- Carry permission metadata into derived stores
- Fail closed when evaluation errors occur
- Set explicit TTLs on cached authorization inputs
- Invalidate caches on grant and revocation events
- Use time-bound elevation for privileged roles
- Record subject, action, resource, decision, and reason
- Run periodic access recertification
- Test cross-tenant and cross-user access attempts
- Add regression tests for every authorization rule
Knowledge Check
What causes role explosion?
Encoding varying dimensions such as region or department into role names, which makes the role count grow as the product of those dimensions rather than their sum.
What does ABAC evaluate?
Attributes of the subject, resource, action, and environment against written policies, rather than checking role membership alone.
Why filter inside the query?
Post-retrieval filtering produces inconsistent page sizes and misleading counts, and it wastes work fetching rows the user cannot see.
Why should the engine fail closed?
An evaluation failure provides no evidence that access is permitted, so allowing the request would grant access on the basis of an error.
Why record the matched policy?
Without knowing which rule produced a decision, investigators cannot explain why access was granted or denied, which undermines audit and incident response.
Summary
RBAC groups permissions into roles that map to job functions. It is understandable, fast to evaluate, and easy to review, but it degrades into role explosion when permissions must vary by region, ownership, classification, or context.
ABAC evaluates policies against subject, resource, action, and environment attributes. It expresses fine-grained and contextual rules without multiplying roles, at the cost of higher evaluation complexity and harder enumeration of who holds access.
ReBAC decides from relationships and suits hierarchical sharing. Most substantial systems combine models: roles for administrative capability, relationships for content, and attributes for contextual restrictions.
Regardless of model, the enforcement discipline is constant. Deny by default, check the specific resource, enforce on the server, push filters into queries, carry permissions into derived stores, fail closed on error, and record enough detail to explain every decision afterwards.
Key Takeaway
Choose the model that matches what your permissions actually depend on. If access varies by job function, use roles. If it varies by data or context, use attributes. If it flows through sharing and containment, use relationships. Then enforce server-side, deny by default, and make every decision explainable.