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:
- Define Roles (e.g., Admin, Editor, Viewer)
- Assign Permissions to roles (e.g., "read:articles", "write:articles", "delete:users")
- Assign Users to 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
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 |
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:resourceorresource: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.