Skip to main content
Resources Security 8 min read

RBAC vs ABAC: Access Control Models Explained

Understand the difference between Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC), and choose the right model for your system.

Access control seems simple until you need to implement it: users have roles, roles have permissions, done. Then the requirements evolve—“managers can approve expenses, but only for their own department” or “users can edit documents they created, but only if the document is in draft status and created less than 30 days ago.”

Suddenly, roles aren’t enough. You start adding more roles (manager_sales, manager_engineering, manager_finance), special-case permissions, and hardcoded logic. Three months later, you have 147 roles and nobody remembers what editor_limited_regional_temporary is supposed to do.

The question isn’t RBAC vs ABAC—it’s when RBAC stops being enough and how to evolve to ABAC without rewriting your entire authorization system. Here’s what actually works.

RBAC: Simple Until It Isn’t

Role-Based Access Control (RBAC) assigns permissions to roles, then assigns roles to users:

# Roles
admin:
  - create_user
  - delete_user
  - view_all_data

editor:
  - create_post
  - edit_post
  - publish_post

viewer:
  - view_post

Users get one or more roles, and permissions are checked based on those roles:

def can_publish_post(user, post):
    return 'editor' in user.roles or 'admin' in user.roles

This works perfectly for:

  • Static permissions (admins can delete, editors can publish, viewers can view)
  • Organization-wide access (all editors can edit any post)
  • Simple hierarchies (admins have all permissions that editors have)

RBAC breaks down when permissions depend on:

  • Context (managers can approve expenses for their own department)
  • Relationships (users can edit documents they created)
  • Resource attributes (users can view draft documents, but published documents require subscription)
  • Time (temporary access that expires)
  • Combinations (users in the “finance” department with “manager” role can approve expenses over $10,000)

Most teams start with RBAC and end up bolting on special cases:

def can_approve_expense(user, expense):
    # RBAC check
    if 'admin' in user.roles:
        return True

    # Special case #1: managers in same department
    if 'manager' in user.roles and user.department == expense.department:
        return True

    # Special case #2: finance team for large expenses
    if 'finance' in user.roles and expense.amount > 10000:
        return True

    # Special case #3: expense creator can approve if delegated
    if user.id == expense.creator_id and expense.delegated_approval:
        return True

    return False

This code works, but it’s not scalable. Every new permission requirement adds another if statement. Auditing what users can actually do becomes impossible because the logic is scattered across your codebase.

ABAC: Flexible But Complex

Attribute-Based Access Control (ABAC) makes decisions based on attributes of:

  • User (role, department, clearance level, location)
  • Resource (owner, status, classification, created_date)
  • Action (read, write, delete, approve)
  • Environment (time, IP address, device type)

Instead of hardcoded roles, you define policies:

# Policy: Managers can approve expenses in their department
- effect: allow
  subject:
    role: manager
    department: $resource.department
  resource:
    type: expense
  action: approve

# Policy: Users can edit their own draft documents
- effect: allow
  subject:
    id: $resource.owner_id
  resource:
    type: document
    status: draft
  action: edit

# Policy: Finance team can view all expenses over $10,000
- effect: allow
  subject:
    department: finance
  resource:
    type: expense
    amount: { $gt: 10000 }
  action: view

The authorization engine evaluates these policies at runtime:

def can_user_perform_action(user, action, resource, context):
    policies = load_policies()

    for policy in policies:
        if matches_policy(user, action, resource, context, policy):
            return policy.effect == 'allow'

    return False  # Default deny

ABAC handles complex scenarios naturally:

  • Data-dependent permissions: “Users can view documents marked as public OR documents they created”
  • Contextual access: “Employees can access the VPN only from registered devices”
  • Dynamic hierarchies: “Managers can approve expenses up to 2× their direct reports’ highest salary”
  • Temporal access: “Contractors have read-only access for 90 days after project completion”

The trade-off: ABAC is harder to implement, harder to debug, and harder to audit than RBAC. When a user can’t access something, figuring out why requires evaluating all policies and understanding which one denied access.

Hybrid Approach: RBAC with Contextual Checks

Most teams don’t need full ABAC. You can extend RBAC with contextual checks:

# Base RBAC check
def has_permission(user, permission):
    for role in user.roles:
        if permission in role.permissions:
            return True
    return False

# Contextual wrapper
def can_approve_expense(user, expense):
    # RBAC: user must have 'approve_expense' permission
    if not has_permission(user, 'approve_expense'):
        return False

    # Contextual: check department match
    if user.department != expense.department:
        return False

    # Contextual: check expense limit based on role
    max_amount = get_approval_limit(user)
    if expense.amount > max_amount:
        return False

    return True

This keeps RBAC simplicity for most permissions while handling special cases explicitly. It’s easier to understand than full ABAC but more maintainable than hardcoded conditionals everywhere.

The downside: authorization logic lives in application code, not in a centralized policy engine. Auditing requires reading code.

When to Use Full ABAC

You need ABAC when:

Permissions depend on data. “Users can view invoices for customers they manage” can’t be expressed in RBAC without creating a role for every user-customer combination.

Compliance requires detailed access policies. Healthcare (HIPAA), finance (SOX), and government systems often require fine-grained access based on data classification, user clearance, time of day, and location.

You have many roles that differ only slightly. If you have regional_manager_northeast, regional_manager_southwest, regional_manager_international, you’re working around RBAC’s limitations. ABAC can express this as a single policy with a region attribute.

Delegation and temporary access are common. “Grant Alice access to Bob’s files while Bob is on vacation” is hard in RBAC (you’d need to change Alice’s role or Bob’s files’ permissions). In ABAC, it’s a policy with a time constraint.

You’re building a multi-tenant platform. SaaS platforms need to enforce tenant isolation (“users can only access data in their own tenant”) plus custom permissions per tenant (“tenant A allows editors to publish; tenant B requires admin approval”). ABAC handles this naturally.

Implementation Patterns

RBAC implementation:

# Database schema
users:
  id, name, email

roles:
  id, name

user_roles:
  user_id, role_id

permissions:
  id, name

role_permissions:
  role_id, permission_id

# Authorization check
def has_permission(user_id, permission_name):
    return db.execute("""
        SELECT 1 FROM users u
        JOIN user_roles ur ON u.id = ur.user_id
        JOIN role_permissions rp ON ur.role_id = rp.role_id
        JOIN permissions p ON rp.permission_id = p.id
        WHERE u.id = ? AND p.name = ?
    """, user_id, permission_name).exists()

Simple, fast, works in any database.

ABAC implementation (using Open Policy Agent):

# Policy in Rego language
package app.authz

default allow = false

# Managers can approve expenses in their department
allow {
    input.user.role == "manager"
    input.user.department == input.resource.department
    input.action == "approve"
    input.resource.type == "expense"
}

# Users can edit their own draft documents
allow {
    input.user.id == input.resource.owner_id
    input.resource.status == "draft"
    input.action == "edit"
}
# Application code
from opa_client import OPA

opa = OPA("http://opa:8181")

def can_approve_expense(user, expense):
    decision = opa.evaluate("app/authz/allow", {
        "user": {
            "id": user.id,
            "role": user.role,
            "department": user.department
        },
        "resource": {
            "type": "expense",
            "id": expense.id,
            "department": expense.department,
            "amount": expense.amount,
            "owner_id": expense.creator_id
        },
        "action": "approve"
    })
    return decision.get("result", False)

Policies are centralized, versioned, and testable. Changes don’t require code deploys.

Migrating from RBAC to ABAC

Don’t do a big-bang rewrite. Migrate incrementally:

Phase 1: Externalize RBAC logic

Move role-permission mappings from database to a policy file:

# policies.yaml
roles:
  admin:
    - users:create
    - users:delete
    - posts:publish
  editor:
    - posts:create
    - posts:edit
    - posts:publish
  viewer:
    - posts:view

Load this at startup. Your application code doesn’t change, but policies are now in one place.

Phase 2: Add attribute-based rules

Keep RBAC for simple permissions, add ABAC for complex cases:

# policies.yaml
rbac_rules:
  admin: [users:create, users:delete, posts:publish]
  editor: [posts:create, posts:edit, posts:publish]

abac_rules:
  - description: "Users can edit their own draft posts"
    effect: allow
    subject: { id: $resource.author_id }
    action: edit
    resource: { type: post, status: draft }

  - description: "Managers approve expenses in their department"
    effect: allow
    subject: { role: manager, department: $resource.department }
    action: approve
    resource: { type: expense }

Check RBAC first (fast), fall back to ABAC for unmatched permissions.

Phase 3: Full ABAC migration

Convert all permissions to attribute-based policies. This is optional—many teams stop at Phase 2.

Debugging and Auditing

The hardest part of ABAC is answering: “Why can’t this user do this thing?”

For RBAC, it’s straightforward:

-- Check user's roles
SELECT r.name FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = 12345;

-- Check role's permissions
SELECT p.name FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = 5;

For ABAC, you need policy evaluation logs:

{
  "user": {"id": 12345, "role": "editor", "department": "engineering"},
  "resource": {"id": 67890, "type": "expense", "department": "sales", "amount": 5000},
  "action": "approve",
  "decision": "deny",
  "evaluated_policies": [
    {
      "policy": "managers_approve_department_expenses",
      "result": "not_applicable",
      "reason": "user.department != resource.department"
    },
    {
      "policy": "finance_approves_large_expenses",
      "result": "not_applicable",
      "reason": "user.department != 'finance'"
    }
  ]
}

Build this into your authorization engine from day one. Without evaluation logs, debugging ABAC policies is nearly impossible.

What We Actually Use

For most projects, we use RBAC with contextual checks:

  • Standard permissions (view, create, edit, delete) are role-based
  • Ownership checks (edit own posts, view own invoices) are explicit conditionals
  • Department/team isolation is enforced at the query level (filter by user’s department)
  • Approval workflows are custom logic (expense approval limits, document review chains)

We implement full ABAC when:

  • Clients need audit trails showing policy compliance (healthcare, finance)
  • Multi-tenant platforms require customizable permissions per tenant
  • Complex delegation and temporary access are core requirements

The key insight: RBAC is enough for 80% of applications. The other 20% need ABAC, but they really need it—and trying to solve those problems with RBAC leads to unmaintainable role explosion.

Start simple. Add complexity only when the pain of working around RBAC’s limitations exceeds the cost of implementing ABAC.


Need help designing an authorization system that scales with your business requirements? We can help.

Have a Project
In Mind?

Let's discuss how we can help you build reliable, scalable systems.