Appearance
RBAC and ABAC — Role-Based and Attribute-Based Access Control
Introduction
Access control determines who can do what within a system. Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC) are the two dominant models for authorizing users in modern applications, APIs, and cloud infrastructure. Understanding both models — their strengths, limitations, and how they complement each other — is essential for designing secure, scalable systems.
Core Concepts
The Authorization Problem
Authentication answers "Who are you?" while authorization answers "What are you allowed to do?". Once a user's identity is established (via OAuth, SSO, certificates, etc.), the system must decide whether to permit or deny each requested action. The access control model you choose shapes your entire security architecture.
Role-Based Access Control (RBAC)
RBAC assigns permissions to roles, and roles are assigned to users. Instead of granting individual permissions to each user, you define roles like Admin, Editor, or Viewer, attach permissions to those roles, and then assign users to the appropriate role(s).
The key entities in RBAC are:
- User: An identity (person or service account)
- Role: A named collection of permissions (e.g.,
billing-admin) - Permission: An allowed action on a resource (e.g.,
invoices:read) - Session: The context in which a user activates one or more roles
RBAC comes in several levels of sophistication defined by the NIST standard:
| Level | Name | Description |
|---|---|---|
| RBAC₀ | Flat RBAC | Users ↔ Roles ↔ Permissions, no hierarchy |
| RBAC₁ | Hierarchical RBAC | Roles can inherit from parent roles |
| RBAC₂ | Constrained RBAC | Adds constraints like mutual exclusion (separation of duties) |
| RBAC₃ | Symmetric RBAC | Combines hierarchy + constraints + permission review |
Attribute-Based Access Control (ABAC)
ABAC makes authorization decisions by evaluating policies against attributes. Attributes can describe anything: the user, the resource, the action, or the environment. Decisions are computed dynamically at request time.
The four attribute categories are:
- Subject attributes: User department, clearance level, job title, group membership
- Resource attributes: Data classification, owner, creation date, sensitivity label
- Action attributes: Read, write, delete, approve
- Environment attributes: Time of day, IP address, device type, geolocation
An ABAC policy might read: "Allow if the subject's department equals the resource's department AND the resource classification is not TOP_SECRET AND the current time is within business hours."
RBAC vs. ABAC — When to Use Which
| Dimension | RBAC | ABAC |
|---|---|---|
| Complexity | Low — easy to understand and audit | High — policies can be intricate |
| Granularity | Coarse (role-level) | Fine-grained (attribute-level) |
| Scalability of policies | Role explosion in complex orgs | Scales well with few policy rules |
| Dynamic context | No (static role assignments) | Yes (time, location, risk score) |
| Audit ease | Simple — enumerate role members | Harder — must trace policy evaluation |
| Setup cost | Low | High (attribute sources, policy engine) |
| Best for | Small-to-medium apps, clear hierarchies | Regulated industries, multi-tenant SaaS |
The XACML Architecture (ABAC Standard)
The OASIS XACML standard defines a reference architecture for ABAC that has influenced nearly every modern policy engine:
- PEP (Policy Enforcement Point): The component that intercepts requests and enforces decisions (e.g., an API gateway, middleware)
- PDP (Policy Decision Point): Evaluates policies against attributes and returns a decision
- PAP (Policy Administration Point): Where administrators author and manage policies
- PIP (Policy Information Point): Retrieves attribute data from external sources (LDAP, databases, HR systems)
Implementation — RBAC in Java
The following example demonstrates a clean RBAC implementation with role hierarchies and permission checking:
java
import java.util.*;
import java.util.stream.Collectors;
public class RbacSystem {
// --- Domain Models ---
public record Permission(String resource, String action) {
@Override
public String toString() {
return resource + ":" + action;
}
}
public static class Role {
private final String name;
private final Set<Permission> permissions = new HashSet<>();
private final Set<Role> parentRoles = new HashSet<>(); // Hierarchy
public Role(String name) {
this.name = name;
}
public Role addPermission(Permission permission) {
permissions.add(permission);
return this;
}
public Role inheritsFrom(Role parent) {
parentRoles.add(parent);
return this;
}
// Collect permissions from this role and all ancestors
public Set<Permission> getEffectivePermissions() {
Set<Permission> effective = new HashSet<>(permissions);
for (Role parent : parentRoles) {
effective.addAll(parent.getEffectivePermissions());
}
return Collections.unmodifiableSet(effective);
}
public String getName() { return name; }
}
public static class User {
private final String username;
private final Set<Role> roles = new HashSet<>();
public User(String username) {
this.username = username;
}
public User assignRole(Role role) {
roles.add(role);
return this;
}
public boolean hasPermission(String resource, String action) {
Permission target = new Permission(resource, action);
return roles.stream()
.flatMap(role -> role.getEffectivePermissions().stream())
.anyMatch(p -> p.equals(target));
}
public Set<Permission> getAllPermissions() {
return roles.stream()
.flatMap(role -> role.getEffectivePermissions().stream())
.collect(Collectors.toUnmodifiableSet());
}
public String getUsername() { return username; }
}
// --- Separation of Duties Constraint ---
public static class MutualExclusionConstraint {
private final Role roleA;
private final Role roleB;
public MutualExclusionConstraint(Role roleA, Role roleB) {
this.roleA = roleA;
this.roleB = roleB;
}
public boolean isViolatedBy(User user) {
boolean hasA = user.roles.contains(roleA);
boolean hasB = user.roles.contains(roleB);
return hasA && hasB;
}
}
// --- Demo ---
public static void main(String[] args) {
// Define permissions
Permission readArticles = new Permission("articles", "read");
Permission writeArticles = new Permission("articles", "write");
Permission deleteArticles = new Permission("articles", "delete");
Permission manageUsers = new Permission("users", "manage");
Permission viewAnalytics = new Permission("analytics", "read");
// Define role hierarchy: Viewer < Editor < Admin
Role viewer = new Role("Viewer")
.addPermission(readArticles);
Role editor = new Role("Editor")
.inheritsFrom(viewer)
.addPermission(writeArticles);
Role admin = new Role("Admin")
.inheritsFrom(editor)
.addPermission(deleteArticles)
.addPermission(manageUsers)
.addPermission(viewAnalytics);
// Assign users
User alice = new User("alice").assignRole(admin);
User bob = new User("bob").assignRole(editor);
User carol = new User("carol").assignRole(viewer);
// Check permissions
System.out.println("=== RBAC Permission Checks ===");
System.out.printf("Alice can delete articles: %s%n",
alice.hasPermission("articles", "delete")); // true
System.out.printf("Bob can delete articles: %s%n",
bob.hasPermission("articles", "delete")); // false
System.out.printf("Bob can read articles: %s%n",
bob.hasPermission("articles", "read")); // true (inherited)
System.out.printf("Carol can write articles: %s%n",
carol.hasPermission("articles", "write")); // false
// Print effective permissions
System.out.println("\n=== Effective Permissions ===");
System.out.printf("Admin permissions: %s%n", alice.getAllPermissions());
System.out.printf("Editor permissions: %s%n", bob.getAllPermissions());
// Separation of duties
Role auditor = new Role("Auditor").addPermission(viewAnalytics);
var constraint = new MutualExclusionConstraint(admin, auditor);
User dave = new User("dave").assignRole(admin).assignRole(auditor);
System.out.printf("%nSoD violation for dave (Admin+Auditor): %s%n",
constraint.isViolatedBy(dave)); // true
}
}Implementation — ABAC Policy Engine in Java
This implementation shows a composable ABAC engine with attribute sources and policy evaluation:
java
import java.time.LocalTime;
import java.util.*;
import java.util.function.Predicate;
public class AbacSystem {
// --- Attribute Context ---
public static class AttributeContext {
private final Map<String, Object> subjectAttrs;
private final Map<String, Object> resourceAttrs;
private final Map<String, Object> actionAttrs;
private final Map<String, Object> environmentAttrs;
public AttributeContext(
Map<String, Object> subject,
Map<String, Object> resource,
Map<String, Object> action,
Map<String, Object> environment) {
this.subjectAttrs = subject;
this.resourceAttrs = resource;
this.actionAttrs = action;
this.environmentAttrs = environment;
}
public Object getSubjectAttr(String key) { return subjectAttrs.get(key); }
public Object getResourceAttr(String key) { return resourceAttrs.get(key); }
public Object getActionAttr(String key) { return actionAttrs.get(key); }
public Object getEnvAttr(String key) { return environmentAttrs.get(key); }
}
// --- Policy Definition ---
public enum Effect { PERMIT, DENY }
public record Policy(
String name,
String description,
Predicate<AttributeContext> condition,
Effect effect
) {}
// --- Policy Decision Point (PDP) ---
public static class PolicyDecisionPoint {
private final List<Policy> policies = new ArrayList<>();
private final Effect defaultEffect;
public PolicyDecisionPoint(Effect defaultEffect) {
this.defaultEffect = defaultEffect;
}
public void addPolicy(Policy policy) {
policies.add(policy);
}
// Deny-overrides: any DENY wins, otherwise first PERMIT wins
public Effect evaluate(AttributeContext context) {
Effect result = null;
for (Policy policy : policies) {
if (policy.condition().test(context)) {
System.out.printf(" [PDP] Policy '%s' matched → %s%n",
policy.name(), policy.effect());
if (policy.effect() == Effect.DENY) {
return Effect.DENY; // Deny-overrides
}
if (result == null) {
result = policy.effect();
}
}
}
Effect finalResult = (result != null) ? result : defaultEffect;
System.out.printf(" [PDP] Final decision: %s%n", finalResult);
return finalResult;
}
}
// --- Policy Enforcement Point (PEP) ---
public static class PolicyEnforcementPoint {
private final PolicyDecisionPoint pdp;
public PolicyEnforcementPoint(PolicyDecisionPoint pdp) {
this.pdp = pdp;
}
public boolean authorize(AttributeContext context) {
Effect decision = pdp.evaluate(context);
return decision == Effect.PERMIT;
}
}
// --- Demo ---
public static void main(String[] args) {
PolicyDecisionPoint pdp = new PolicyDecisionPoint(Effect.DENY);
// Policy 1: Same department can read documents
pdp.addPolicy(new Policy(
"same-department-read",
"Users can read resources in their own department",
ctx -> {
String userDept = (String) ctx.getSubjectAttr("department");
String resDept = (String) ctx.getResourceAttr("department");
String action = (String) ctx.getActionAttr("type");
return "read".equals(action) && userDept != null && userDept.equals(resDept);
},
Effect.PERMIT
));
// Policy 2: Only managers can write
pdp.addPolicy(new Policy(
"managers-can-write",
"Managers can write to resources in their department",
ctx -> {
String role = (String) ctx.getSubjectAttr("title");
String userDept = (String) ctx.getSubjectAttr("department");
String resDept = (String) ctx.getResourceAttr("department");
String action = (String) ctx.getActionAttr("type");
return "write".equals(action)
&& "manager".equals(role)
&& userDept != null && userDept.equals(resDept);
},
Effect.PERMIT
));
// Policy 3: Deny access outside business hours
pdp.addPolicy(new Policy(
"business-hours-only",
"Deny all access outside 08:00-18:00",
ctx -> {
LocalTime time = (LocalTime) ctx.getEnvAttr("currentTime");
return time != null &&
(time.isBefore(LocalTime.of(8, 0)) || time.isAfter(LocalTime.of(18, 0)));
},
Effect.DENY
));
// Policy 4: Deny access to top-secret resources unless clearance matches
pdp.addPolicy(new Policy(
"top-secret-clearance",
"Deny access to TOP_SECRET unless user has top_secret clearance",
ctx -> {
String classification = (String) ctx.getResourceAttr("classification");
String clearance = (String) ctx.getSubjectAttr("clearance");
return "TOP_SECRET".equals(classification)
&& !"top_secret".equals(clearance);
},
Effect.DENY
));
PolicyEnforcementPoint pep = new PolicyEnforcementPoint(pdp);
// Scenario 1: Engineer reads own department doc during business hours
System.out.println("\n=== Scenario 1: Engineer reads own dept doc ===");
boolean allowed1 = pep.authorize(new AttributeContext(
Map.of("username", "alice", "department", "engineering",
"title", "engineer", "clearance", "confidential"),
Map.of("id", "doc-123", "department", "engineering",
"classification", "INTERNAL"),
Map.of("type", "read"),
Map.of("currentTime", LocalTime.of(14, 30))
));
System.out.println("Result: " + (allowed1 ? "ALLOWED" : "DENIED"));
// Scenario 2: Engineer tries to write (not a manager)
System.out.println("\n=== Scenario 2: Engineer tries to write ===");
boolean allowed2 = pep.authorize(new AttributeContext(
Map.of("username", "alice", "department", "engineering",
"title", "engineer", "clearance", "confidential"),
Map.of("id", "doc-123", "department", "engineering",
"classification", "INTERNAL"),
Map.of("type", "write"),
Map.of("currentTime", LocalTime.of(14, 30))
));
System.out.println("Result: " + (allowed2 ? "ALLOWED" : "DENIED"));
// Scenario 3: Manager writes during business hours
System.out.println("\n=== Scenario 3: Manager writes ===");
boolean allowed3 = pep.authorize(new AttributeContext(
Map.of("username", "bob", "department", "engineering",
"title", "manager", "clearance", "secret"),
Map.of("id", "doc-456", "department", "engineering",
"classification", "CONFIDENTIAL"),
Map.of("type", "write"),
Map.of("currentTime", LocalTime.of(10, 0))
));
System.out.println("Result: " + (allowed3 ? "ALLOWED" : "DENIED"));
// Scenario 4: Access at 2 AM — denied by business hours policy
System.out.println("\n=== Scenario 4: Access at 2 AM ===");
boolean allowed4 = pep.authorize(new AttributeContext(
Map.of("username", "alice", "department", "engineering",
"title", "engineer", "clearance", "confidential"),
Map.of("id", "doc-123", "department", "engineering",
"classification", "INTERNAL"),
Map.of("type", "read"),
Map.of("currentTime", LocalTime.of(2, 0))
));
System.out.println("Result: " + (allowed4 ? "ALLOWED" : "DENIED"));
// Scenario 5: Accessing TOP_SECRET without clearance
System.out.println("\n=== Scenario 5: TOP_SECRET without clearance ===");
boolean allowed5 = pep.authorize(new AttributeContext(
Map.of("username", "carol", "department", "engineering",
"title", "manager", "clearance", "confidential"),
Map.of("id", "doc-999", "department", "engineering",
"classification", "TOP_SECRET"),
Map.of("type", "read"),
Map.of("currentTime", LocalTime.of(12, 0))
));
System.out.println("Result: " + (allowed5 ? "ALLOWED" : "DENIED"));
}
}Hybrid Approach — RBAC + ABAC
In practice, most production systems combine both models. RBAC provides the structural backbone (roles, coarse permissions), while ABAC adds contextual, fine-grained decisions on top.
Here is a Java implementation of the hybrid approach:
java
import java.time.LocalTime;
import java.util.*;
import java.util.function.Predicate;
public class HybridAccessControl {
// Reuse RBAC User and Role from the previous example
public record Permission(String resource, String action) {}
public static class Role {
private final String name;
private final Set<Permission> permissions = new HashSet<>();
public Role(String name) { this.name = name; }
public Role addPermission(Permission p) { permissions.add(p); return this; }
public Set<Permission> getPermissions() { return permissions; }
}
public static class User {
private final String username;
private final Map<String, Object> attributes;
private final Set<Role> roles = new HashSet<>();
public User(String username, Map<String, Object> attributes) {
this.username = username;
this.attributes = attributes;
}
public User assignRole(Role r) { roles.add(r); return this; }
public boolean hasRolePermission(String resource, String action) {
Permission target = new Permission(resource, action);
return roles.stream()
.flatMap(r -> r.getPermissions().stream())
.anyMatch(p -> p.equals(target));
}
public Map<String, Object> getAttributes() { return attributes; }
public String getUsername() { return username; }
}
// Lightweight ABAC condition
@FunctionalInterface
public interface AbacCondition {
boolean evaluate(User user, Map<String, Object> resourceAttrs,
Map<String, Object> envAttrs);
}
// Hybrid authorization service
public static class HybridAuthorizer {
private final List<AbacCondition> denyConditions = new ArrayList<>();
public void addDenyCondition(AbacCondition condition) {
denyConditions.add(condition);
}
public boolean authorize(User user, String resource, String action,
Map<String, Object> resourceAttrs,
Map<String, Object> envAttrs) {
// Step 1: RBAC gate
if (!user.hasRolePermission(resource, action)) {
System.out.printf(" [RBAC] %s lacks %s:%s permission → DENIED%n",
user.getUsername(), resource, action);
return false;
}
System.out.printf(" [RBAC] %s has %s:%s permission → PASSED%n",
user.getUsername(), resource, action);
// Step 2: ABAC refinement
for (AbacCondition condition : denyConditions) {
if (condition.evaluate(user, resourceAttrs, envAttrs)) {
System.out.printf(" [ABAC] Deny condition triggered → DENIED%n");
return false;
}
}
System.out.printf(" [ABAC] All conditions passed → ALLOWED%n");
return true;
}
}
public static void main(String[] args) {
// Setup roles
Role editor = new Role("editor")
.addPermission(new Permission("documents", "read"))
.addPermission(new Permission("documents", "write"));
// Setup users with attributes
User alice = new User("alice", Map.of(
"department", "engineering",
"clearance", "secret"
)).assignRole(editor);
User bob = new User("bob", Map.of(
"department", "marketing",
"clearance", "public"
)).assignRole(editor);
// Setup hybrid authorizer
HybridAuthorizer authorizer = new HybridAuthorizer();
// ABAC rule: deny cross-department writes
authorizer.addDenyCondition((user, resAttrs, envAttrs) -> {
String userDept = (String) user.getAttributes().get("department");
String resDept = (String) resAttrs.get("department");
String action = (String) envAttrs.getOrDefault("action", "");
return "write".equals(action) && !userDept.equals(resDept);
});
// ABAC rule: deny access outside business hours
authorizer.addDenyCondition((user, resAttrs, envAttrs) -> {
LocalTime now = (LocalTime) envAttrs.get("currentTime");
return now.isBefore(LocalTime.of(8, 0)) || now.isAfter(LocalTime.of(18, 0));
});
Map<String, Object> engDoc = Map.of("department", "engineering", "id", "doc-1");
Map<String, Object> mktDoc = Map.of("department", "marketing", "id", "doc-2");
// Alice writes to engineering doc (allowed)
System.out.println("\n=== Alice writes engineering doc ===");
authorizer.authorize(alice, "documents", "write", engDoc,
Map.of("action", "write", "currentTime", LocalTime.of(10, 0)));
// Bob writes to engineering doc (denied — cross-department)
System.out.println("\n=== Bob writes engineering doc ===");
authorizer.authorize(bob, "documents", "write", engDoc,
Map.of("action", "write", "currentTime", LocalTime.of(10, 0)));
// Alice reads engineering doc at 2 AM (denied — outside hours)
System.out.println("\n=== Alice reads at 2 AM ===");
authorizer.authorize(alice, "documents", "read", engDoc,
Map.of("action", "read", "currentTime", LocalTime.of(2, 0)));
}
}RBAC and ABAC in AWS
AWS IAM is a prime real-world example of a hybrid RBAC/ABAC system:
An AWS IAM policy combining RBAC (attached to a role) with ABAC conditions:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSameDepartmentS3Access",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::company-data/*",
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/department": "${aws:PrincipalTag/department}"
},
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
},
"DateGreaterThan": {
"aws:CurrentTime": "2024-01-01T08:00:00Z"
}
}
}
]
}The Role Explosion Problem
One of RBAC's biggest challenges is role explosion — the combinatorial growth of roles as organizational requirements become more nuanced:
ABAC solves this elegantly: instead of 27 roles, you use 3 base roles with attribute-based policies that check department and geography tags dynamically.
Policy Combining Algorithms
When multiple policies apply to a single request, the system needs a strategy to combine their results:
Best Practices
- Start with RBAC, layer ABAC: Begin with role-based controls for simplicity; add attribute-based policies only when you need dynamic, context-aware decisions.
- Principle of least privilege: Assign the minimum permissions necessary — applicable to both roles and attribute policies.
- Avoid role explosion proactively: If you find yourself creating roles that encode dimensions like department + region + clearance, switch to ABAC for those dimensions.
- Enforce separation of duties: Use mutual-exclusion constraints to prevent conflicting roles (e.g., a user cannot be both "Approver" and "Requester").
- Default deny: Always make the default decision a DENY; only explicitly granted permissions should allow access.
- Centralize policy evaluation: Use a dedicated Policy Decision Point (PDP) rather than scattering authorization logic across services.
- Audit continuously: Log every authorization decision with the full context (user, resource, action, decision, policies evaluated) for compliance and debugging.
- Version and test policies: Treat policies as code — store them in version control, write unit tests, and deploy via CI/CD.
- Use tags consistently: For ABAC in cloud environments (AWS, Azure), establish a tagging strategy and enforce it through tag policies.
- Cache wisely: Cache RBAC role lookups aggressively (they change infrequently), but be cautious caching ABAC decisions that depend on volatile attributes like time or IP.
Related Concepts
- OAuth: Often used alongside RBAC/ABAC — OAuth handles delegation and token issuance, while RBAC/ABAC handle the authorization decisions within the protected system.
- AWS Single Sign-On (AWS SSO): Demonstrates enterprise-scale RBAC with permission sets mapped to AWS accounts and IAM roles.
- Cryptography: Underpins the secure transmission and storage of tokens (JWTs, SAML assertions) that carry role and attribute claims used by access control systems.
- REST HTTP Verbs and Status Codes: The 401 Unauthorized and 403 Forbidden status codes are the HTTP-level manifestation of authentication and authorization failures respectively.