Skip to content

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:

LevelNameDescription
RBAC₀Flat RBACUsers ↔ Roles ↔ Permissions, no hierarchy
RBAC₁Hierarchical RBACRoles can inherit from parent roles
RBAC₂Constrained RBACAdds constraints like mutual exclusion (separation of duties)
RBAC₃Symmetric RBACCombines 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

DimensionRBACABAC
ComplexityLow — easy to understand and auditHigh — policies can be intricate
GranularityCoarse (role-level)Fine-grained (attribute-level)
Scalability of policiesRole explosion in complex orgsScales well with few policy rules
Dynamic contextNo (static role assignments)Yes (time, location, risk score)
Audit easeSimple — enumerate role membersHarder — must trace policy evaluation
Setup costLowHigh (attribute sources, policy engine)
Best forSmall-to-medium apps, clear hierarchiesRegulated 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

  1. Start with RBAC, layer ABAC: Begin with role-based controls for simplicity; add attribute-based policies only when you need dynamic, context-aware decisions.
  2. Principle of least privilege: Assign the minimum permissions necessary — applicable to both roles and attribute policies.
  3. Avoid role explosion proactively: If you find yourself creating roles that encode dimensions like department + region + clearance, switch to ABAC for those dimensions.
  4. Enforce separation of duties: Use mutual-exclusion constraints to prevent conflicting roles (e.g., a user cannot be both "Approver" and "Requester").
  5. Default deny: Always make the default decision a DENY; only explicitly granted permissions should allow access.
  6. Centralize policy evaluation: Use a dedicated Policy Decision Point (PDP) rather than scattering authorization logic across services.
  7. Audit continuously: Log every authorization decision with the full context (user, resource, action, decision, policies evaluated) for compliance and debugging.
  8. Version and test policies: Treat policies as code — store them in version control, write unit tests, and deploy via CI/CD.
  9. Use tags consistently: For ABAC in cloud environments (AWS, Azure), establish a tagging strategy and enforce it through tag policies.
  10. 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.
  • 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.