Table of Contents

    RBAC/ABAC

    SYSTEM DESIGN • CHAPTER 13.4

    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.

    Learning objective: By the end of this article, you will understand RBAC structure and role explosion, ABAC policy evaluation and its costs, the relationship-based alternative, enforcement placement, and how to keep authorization decisions auditable and performant.

    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.

    THE DECISION
    Subject Action Resource Permit or Deny

    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.

    A valid token proves identity. It says nothing about whether this particular user may open this particular record.

    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.

    RBAC STRUCTURE
    User Role Permission Operation

    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
    Inheritance caution: Deep hierarchies make it difficult to answer why a user holds a permission. Keep inheritance shallow enough that an auditor can trace any grant in a few steps.

    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.

    COMBINATORIAL GROWTH
    \[ N_{\text{roles}} = \prod_{i=1}^{k} d_i \]

    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.

    Symptoms of Role Explosion Role names encoding location or department, hundreds of roles with near-identical permission sets, roles created per customer or project, and nobody able to explain what a given role grants.
    Structural Fix Move the varying dimensions out of the role name and into attributes or scoped assignments, so one role can be granted within many contexts.

    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
    POLICY EVALUATION
    \[ Decision = f(S, R, A, E) \]

    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
    Practical note: These models are not mutually exclusive. A realistic system often uses roles for administrative capability, relationships for content sharing, and attributes for contextual restrictions.

    Authorization Architecture

    Separating the decision point from the enforcement point keeps policy logic out of scattered application code.

    1

    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.

    2

    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.

    3

    Policy Information Point

    Where attributes come from.

    Supplies subject, resource, and environment attributes from directories, databases, and request context.

    4

    Policy Administration Point

    Where rules are authored and versioned.

    Manages policy lifecycle, including review, approval, deployment, and rollback.

    DECISION FLOW
    Request Enforcement Point Decision Point Permit or Deny

    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
    Client-Side Only Hiding a button in the interface while the underlying endpoint remains callable. The control is cosmetic, and the API is fully exposed to anyone who inspects it.
    Server-Side Authoritative The interface hides unavailable actions for usability, while the server independently verifies every request regardless of what the client displayed.

    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);
    }
    Response choice: Returning not-found rather than forbidden for unauthorized resources avoids confirming that a given identifier exists, which matters when identifiers are guessable.

    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.

    Filter After Fetch Retrieve fifty records, remove the ones the user cannot see, and return the remainder. Page sizes become inconsistent and totals become misleading.
    Filter In Query Translate the authorization rule into query predicates so the database returns only permitted rows, keeping pagination and counts accurate.
    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.

    DECISION COST
    \[ T_{\text{authz}} = T_{\text{attributes}} + T_{\text{evaluation}} + T_{\text{audit}} \]
    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
    CACHING RULE
    Every cached authorization input creates a window in which revoked access still succeeds. Choose the TTL deliberately and document it as a security property, not an implementation detail.

    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
    Safe Default Deny unless explicitly permitted, and let explicit denials override permits. A policy engine failure should produce a denial, never an accidental allow.

    Least Privilege and Separation of Duties

    1

    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.

    2

    Separation of Duties

    Ensure that a single identity cannot both initiate and approve a sensitive operation, such as creating a payment and authorizing it.

    3

    Time-Bound Elevation

    Grant elevated permissions temporarily with automatic expiry, rather than permanently assigning administrative roles.

    4

    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
    An authorization system you cannot query is an authorization system you cannot audit. Being able to answer who can access what is a design requirement, not an afterthought.

    Choosing a Model

    Choose RBAC When Permissions align with job functions, the organizational structure is stable, non-technical stakeholders must review access, and record-level conditions are rare.
    Choose ABAC When Access depends on data classification, ownership, region, time, device posture, or any dimension that would otherwise multiply the role count.
    Choose ReBAC When Permission flows through containment or sharing, such as nested folders, teams, and documents with inherited access.
    Combine Them When Roles define administrative capability, attributes apply contextual restrictions, and relationships govern content sharing, which describes most substantial platforms.

    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

    1

    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.

    2

    What does ABAC evaluate?

    Attributes of the subject, resource, action, and environment against written policies, rather than checking role membership alone.

    3

    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.

    4

    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.

    5

    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.