Authorization Pattern

Role-Based Access Control (RBAC)

Master RBAC patterns for secure, scalable authorization. Learn role hierarchies, permission management, enforcement strategies, and implementation best practices.

What is RBAC?

Role-Based Access Control grants permissions based on roles assigned to users, simplifying permission management at scale.

RBAC is the most widely adopted authorization model in enterprise systems. Instead of assigning permissions directly to individual users, you:

  1. Define Roles (e.g., Admin, Editor, Viewer)
  2. Assign Permissions to roles (e.g., "read:articles", "write:articles", "delete:users")
  3. Assign Users to roles
Key Principle: Users → Roles → Permissions. Permissions are never assigned directly to users, only through roles.

Why RBAC?

  • Simplified Management: Change role permissions once, affect all users with that role
  • Least Privilege: Grant minimum necessary permissions through role membership
  • Auditing: Easily answer "Who has access to X?" by checking role membership
  • Compliance: Map organizational job functions to roles for SOX, HIPAA, etc
  • Scalability: Manage millions of users with dozens of roles

RBAC Models

RBAC has evolved into several models with increasing complexity and flexibility.

1. Flat RBAC (Core RBAC)

Structure: Users → Roles → Permissions (simple many-to-many relationships)

Example: A "Blog Editor" role has permissions: read:posts, write:posts, publish:posts

Best for: Simple applications with clear role boundaries

2. Hierarchical RBAC

Structure: Roles can inherit permissions from parent roles

Example: Admin inherits all permissions from Editor, which inherits from Viewer

Benefit: Reduces duplication. Define base permissions once, extend with additional permissions

Best for: Organizations with clear hierarchies (Manager > Employee > Guest)

3. Constrained RBAC

Feature: Add constraints like "mutually exclusive roles" (separation of duties)

Example: A user cannot be both "Purchaser" and "Approver" (prevents self-approval fraud)

Best for: Financial systems, compliance-heavy industries

4. Hybrid RBAC + ABAC

Combine: Role membership + dynamic attributes for fine-grained control

Example: Role "Editor" + condition "resource.owner == user.id" allows editing only own content

Best for: Multi-tenant systems, resource-level permissions

Data Model & Schema

A typical relational database schema for RBAC with role hierarchy support.

Core Tables

-- Users table
CREATE TABLE users (
  id UUID PRIMARY KEY,
  username VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Roles table
CREATE TABLE roles (
  id UUID PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL,
  description TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Permissions table
CREATE TABLE permissions (
  id UUID PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL,  -- e.g., "read:articles", "delete:users"
  description TEXT,
  resource VARCHAR(100),  -- e.g., "articles", "users"
  action VARCHAR(50)      -- e.g., "read", "write", "delete"
);

-- User-Role assignments (many-to-many)
CREATE TABLE user_roles (
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
  assigned_at TIMESTAMP DEFAULT NOW(),
  assigned_by UUID REFERENCES users(id),
  PRIMARY KEY (user_id, role_id)
);

-- Role-Permission assignments (many-to-many)
CREATE TABLE role_permissions (
  role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
  permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
  granted_at TIMESTAMP DEFAULT NOW(),
  PRIMARY KEY (role_id, permission_id)
);

-- Role hierarchy (for hierarchical RBAC)
CREATE TABLE role_hierarchy (
  parent_role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
  child_role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
  PRIMARY KEY (parent_role_id, child_role_id),
  CHECK (parent_role_id != child_role_id)  -- Prevent self-reference
);

-- Indexes for performance
CREATE INDEX idx_user_roles_user ON user_roles(user_id);
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id);

Implementation in Python

A production-ready RBAC system with role hierarchy and caching.

RBAC Service Class

import redis
import json
from typing import Set, List, Optional
from dataclasses import dataclass

@dataclass
class Permission:
    name: str
    resource: str
    action: str

class RBACService:
    def __init__(self, db_connection, redis_client=None):
        self.db = db_connection
        self.cache = redis_client or redis.StrictRedis(decode_responses=True)
        self.cache_ttl = 3600  # 1 hour
    
    def check_permission(self, user_id: str, permission_name: str) -> bool:
        """
        Check if user has a specific permission (through any of their roles).
        Uses caching for performance.
        """
        # Try cache first
        cache_key = f"user_permissions:{user_id}"
        cached_permissions = self.cache.get(cache_key)
        
        if cached_permissions:
            permissions = json.loads(cached_permissions)
            return permission_name in permissions
        
        # Cache miss - query database
        permissions = self.get_user_permissions(user_id)
        
        # Store in cache
        self.cache.setex(
            cache_key,
            self.cache_ttl,
            json.dumps(list(permissions))
        )
        
        return permission_name in permissions
    
    def get_user_permissions(self, user_id: str) -> Set[str]:
        """
        Get all effective permissions for a user (including inherited permissions).
        """
        query = """
        WITH RECURSIVE role_tree AS (
          -- Base: user's direct roles
          SELECT ur.role_id
          FROM user_roles ur
          WHERE ur.user_id = %s
          
          UNION
          
          -- Recursive: inherited roles
          SELECT rh.parent_role_id
          FROM role_hierarchy rh
          INNER JOIN role_tree rt ON rt.role_id = rh.child_role_id
        )
        SELECT DISTINCT p.name
        FROM permissions p
        INNER JOIN role_permissions rp ON rp.permission_id = p.id
        INNER JOIN role_tree rt ON rt.role_id = rp.role_id
        """
        
        cursor = self.db.cursor()
        cursor.execute(query, (user_id,))
        permissions = {row[0] for row in cursor.fetchall()}
        cursor.close()
        
        return permissions
    
    def assign_role(self, user_id: str, role_id: str, assigned_by: str):
        """Assign a role to a user and invalidate cache."""
        query = """
        INSERT INTO user_roles (user_id, role_id, assigned_by)
        VALUES (%s, %s, %s)
        ON CONFLICT (user_id, role_id) DO NOTHING
        """
        cursor = self.db.cursor()
        cursor.execute(query, (user_id, role_id, assigned_by))
        self.db.commit()
        cursor.close()
        
        # Invalidate user permission cache
        self.cache.delete(f"user_permissions:{user_id}")
    
    def revoke_role(self, user_id: str, role_id: str):
        """Revoke a role from a user and invalidate cache."""
        query = "DELETE FROM user_roles WHERE user_id = %s AND role_id = %s"
        cursor = self.db.cursor()
        cursor.execute(query, (user_id, role_id))
        self.db.commit()
        cursor.close()
        
        # Invalidate cache
        self.cache.delete(f"user_permissions:{user_id}")
    
    def has_role(self, user_id: str, role_name: str) -> bool:
        """Check if user has a specific role."""
        query = """
        SELECT 1
        FROM user_roles ur
        INNER JOIN roles r ON r.id = ur.role_id
        WHERE ur.user_id = %s AND r.name = %s
        """
        cursor = self.db.cursor()
        cursor.execute(query, (user_id, role_name))
        result = cursor.fetchone() is not None
        cursor.close()
        return result

# Usage Example
rbac = RBACService(db_connection, redis_client)

# Check permission
if rbac.check_permission(user_id="123", permission_name="delete:users"):
    # User can delete users
    pass

# Assign role
rbac.assign_role(
    user_id="123",
    role_id="admin-role-uuid",
    assigned_by="super-admin-uuid"
)

Decorator for Route Protection (Flask)

from functools import wraps
from flask import request, jsonify, g

rbac = RBACService(db, redis_client)

def require_permission(permission_name: str):
    """Decorator to enforce permission checks on routes."""
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Assume user_id is set by authentication middleware
            user_id = g.get('user_id')
            if not user_id:
                return jsonify({"error": "Unauthorized"}), 401
            
            if not rbac.check_permission(user_id, permission_name):
                return jsonify({
                    "error": "Forbidden",
                    "message": f"Required permission: {permission_name}"
                }), 403
            
            return f(*args, **kwargs)
        return wrapper
    return decorator

# Usage in routes
@app.route('/admin/users', methods=['DELETE'])
@require_permission('delete:users')
def delete_user():
    # Only users with "delete:users" permission can execute this
    return jsonify({"message": "User deleted"})

@app.route('/articles/', methods=['PUT'])
@require_permission('write:articles')
def update_article(article_id):
    return jsonify({"message": "Article updated"})

Enforcement Points

Where to check permissions in your application architecture.

1. API Gateway / Edge

Coarse-grained checks before requests reach backend services. Blocks unauthorized users early.

Example: Block all non-admin requests to /admin/*

2. Service Layer

Fine-grained checks within business logic. Closest to data, most accurate.

Example: Check if user can edit this specific article

3. Database Level

Row-Level Security (RLS) in PostgreSQL for per-row access control.

Example: Users only see rows where user_id = current_user

4. UI/Frontend

Hide UI elements user cannot access (buttons, menu items). Never rely on this alone!

Example: Hide "Delete" button for Viewer role

⚠️ Defense in Depth: Always enforce at multiple layers. Frontend hiding is UX, NOT security. Backend must always validate.

RBAC vs ABAC Comparison

RBAC (Role-Based) vs ABAC (Attribute-Based) Access Control—when to use each.

Dimension RBAC ABAC
Access Decision Based on user's role membership Based on attributes (user, resource, environment)
Flexibility Coarse-grained. Good for static org structures. Fine-grained. Dynamic, context-aware policies.
Complexity Simple to understand and implement. Complex. Requires policy engine and attribute management.
Performance Fast. Simple table lookups, easy to cache. Slower. Requires policy evaluation at runtime.
Management Easy for admins. Assign users to roles. Harder. Define complex rules and attribute mappings.
Example Rule "Editors can write articles" "Users can edit articles if they are the author AND it's before 5pm AND article is not published"
Best For Enterprise apps, clear org hierarchies, static permissions Multi-tenant SaaS, resource-level permissions, dynamic policies
Recommendation: Start with RBAC. Add ABAC rules only when you need fine-grained, context-aware permissions that roles can't express.

Scaling & Performance

Strategies to handle RBAC at scale with millions of users and billions of permission checks.

1. Aggressive Caching

  • Cache User Permissions: Precompute all effective permissions per user, cache in Redis with TTL
  • Cache Role Permissions: Cache role → permissions mapping (changes infrequently)
  • TTL Strategy: Short TTL (5-15 min) for critical roles, longer for regular users
  • Invalidation: On role assignment/revoke, delete user's permission cache key

2. Precompute Role Hierarchies

For deep role hierarchies, precompute flattened permissions offline:

# Offline job to flatten role hierarchies
def precompute_effective_permissions():
    """Run nightly to compute flattened role permissions."""
    for role in get_all_roles():
        permissions = compute_inherited_permissions(role.id)
        cache.setex(
            f"role_permissions:{role.id}",
            86400,  # 24 hours
            json.dumps(list(permissions))
        )

3. Bloom Filters for Negative Checks

Use Bloom filters for quick "definitely does NOT have permission" checks:

from pybloom_live import BloomFilter

def build_user_permission_bloom(user_id):
    permissions = get_user_permissions(user_id)
    bloom = BloomFilter(capacity=1000, error_rate=0.001)
    for perm in permissions:
        bloom.add(perm)
    return bloom

# Quick negative check
if permission not in user_bloom_filter:
    return False  # Definitely don't have permission
else:
    # Might have permission, check cache/DB
    return check_permission_in_cache(user_id, permission)

4. Sharding by Tenant

For multi-tenant SaaS, shard RBAC data by tenant ID. Each tenant's roles/permissions in separate database or schema.

Best Practices

Permission Naming Conventions

Use consistent, hierarchical naming:

  • Format: action:resource or resource:action
  • Examples: read:articles, write:users, delete:comments
  • Wildcards: *:articles (all actions on articles), admin:* (all admin permissions)

Role Design Guidelines

  • Keep roles coarse-grained. 10-50 roles, not 1000s
  • Map roles to job functions, not individuals (e.g., "Content Editor", not "John's Special Role")
  • Use composable roles. "Editor" + "Reviewer" instead of "Editor-Reviewer" mega-role
  • Avoid role explosion. If you need hundreds of roles, consider ABAC for those cases

Audit Everything

# Log all permission changes
def audit_role_assignment(user_id, role_id, assigned_by):
    audit_log = {
        "event": "role_assigned",
        "user_id": user_id,
        "role_id": role_id,
        "assigned_by": assigned_by,
        "timestamp": datetime.utcnow().isoformat()
    }
    # Send to audit log (append-only, immutable)
    append_to_audit_log(audit_log)

Least Privilege Principle

  • Default deny: no permissions unless explicitly granted
  • Time-bound roles: temporary elevated access with expiration
  • Regular access reviews: audit who has what permissions quarterly

Summary

  • RBAC simplifies permission management by grouping permissions into roles.
  • Use role hierarchies to avoid duplication and model org structures.
  • Enforce permissions at multiple layers: API gateway, service, database.
  • Cache aggressively: precompute effective permissions, use Redis, invalidate on changes.
  • RBAC is simpler than ABAC; start with RBAC, add ABAC only when needed.
  • Follow least privilege, audit all changes, and use consistent naming conventions.
  • For scale: cache permissions, flatten hierarchies offline, use Bloom filters for negative checks.