Rbac
Role-based access control design, permission hierarchies, and enforcement patterns
You are an expert in role-based access control for securing applications. You understand role hierarchies, permission modeling, enforcement strategies, and how RBAC integrates with authentication systems. ## Key Points - **User**: An authenticated identity that is assigned roles. - **Role**: A named collection of permissions (e.g., `admin`, `editor`, `viewer`). - **Permission**: A specific action on a resource (e.g., `posts:write`, `users:delete`). - **Role Hierarchy**: A structure where higher roles inherit permissions from lower roles (e.g., `admin` inherits all `editor` permissions). - **Role Assignment**: The mapping of users to roles, often scoped to an organization or resource. - **Least Privilege**: Assigning users the minimum roles necessary to perform their tasks. - **Separation of Duties**: Ensuring that critical operations require multiple roles, preventing a single user from performing an entire sensitive workflow. 1. **Model permissions as `resource:action` pairs** rather than arbitrary strings. This makes the permission model predictable and auditable. 2. **Use role hierarchies** to avoid duplicating permission assignments across related roles. 3. **Scope roles to a context** (organization, project, team) rather than making all roles global. This supports multi-tenant applications. 4. **Cache resolved permissions** per session or with a short TTL to avoid repeated database lookups on every request. 5. **Invalidate the permission cache** when roles or assignments change (publish a cache-bust event).
skilldb get auth-patterns-skills/RbacFull skill: 196 linesRole-Based Access Control (RBAC) — Authentication & Authorization
You are an expert in role-based access control for securing applications. You understand role hierarchies, permission modeling, enforcement strategies, and how RBAC integrates with authentication systems.
Core Philosophy
Overview
Role-Based Access Control assigns permissions to roles rather than directly to users. Users are assigned one or more roles, and they inherit the permissions associated with those roles. RBAC simplifies permission management by providing an abstraction layer — instead of managing per-user permissions across a system, administrators manage role assignments. RBAC is the most widely adopted authorization model in enterprise software.
Core Concepts
- User: An authenticated identity that is assigned roles.
- Role: A named collection of permissions (e.g.,
admin,editor,viewer). - Permission: A specific action on a resource (e.g.,
posts:write,users:delete). - Role Hierarchy: A structure where higher roles inherit permissions from lower roles (e.g.,
admininherits alleditorpermissions). - Role Assignment: The mapping of users to roles, often scoped to an organization or resource.
- Least Privilege: Assigning users the minimum roles necessary to perform their tasks.
- Separation of Duties: Ensuring that critical operations require multiple roles, preventing a single user from performing an entire sensitive workflow.
Implementation Patterns
Permission and Role Schema (PostgreSQL)
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource VARCHAR(100) NOT NULL, -- e.g., 'posts', 'users', 'billing'
action VARCHAR(50) NOT NULL, -- e.g., 'read', 'write', 'delete', 'admin'
UNIQUE (resource, action)
);
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
parent_role_id UUID REFERENCES roles(id) -- For role hierarchy
);
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
scope_type VARCHAR(50), -- e.g., 'organization', 'project'
scope_id UUID, -- The specific org/project ID
PRIMARY KEY (user_id, role_id, scope_type, scope_id)
);
-- Recursive query to resolve inherited permissions
CREATE OR REPLACE FUNCTION get_role_permissions(target_role_id UUID)
RETURNS TABLE(resource VARCHAR, action VARCHAR) AS $$
WITH RECURSIVE role_chain AS (
SELECT id, parent_role_id FROM roles WHERE id = target_role_id
UNION ALL
SELECT r.id, r.parent_role_id FROM roles r
JOIN role_chain rc ON r.id = rc.parent_role_id
)
SELECT DISTINCT p.resource, p.action
FROM role_chain rc
JOIN role_permissions rp ON rp.role_id = rc.id
JOIN permissions p ON p.id = rp.permission_id;
$$ LANGUAGE SQL STABLE;
Middleware Enforcement (Node.js / Express)
// Load user permissions once per request (cache in session or short-lived cache)
async function loadPermissions(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'Unauthenticated' });
const cacheKey = `permissions:${req.user.id}`;
let permissions = await cache.get(cacheKey);
if (!permissions) {
permissions = await db.query(
`SELECT DISTINCT p.resource, p.action
FROM user_roles ur
JOIN LATERAL get_role_permissions(ur.role_id) p ON true
WHERE ur.user_id = $1`,
[req.user.id]
);
await cache.set(cacheKey, permissions, { ttl: 300 }); // 5-minute cache
}
req.permissions = new Set(permissions.map(p => `${p.resource}:${p.action}`));
next();
}
// Permission check helper
function requirePermission(...required) {
return (req, res, next) => {
for (const perm of required) {
if (!req.permissions.has(perm)) {
return res.status(403).json({
error: 'Forbidden',
required: perm,
});
}
}
next();
};
}
// Usage in routes
app.get('/api/posts', loadPermissions, requirePermission('posts:read'), listPosts);
app.post('/api/posts', loadPermissions, requirePermission('posts:write'), createPost);
app.delete('/api/posts/:id', loadPermissions, requirePermission('posts:delete'), deletePost);
app.get('/api/admin/users', loadPermissions, requirePermission('users:admin'), listUsers);
Scoped Roles (Organization-Level RBAC)
from functools import wraps
from flask import g, abort
def require_org_permission(resource: str, action: str):
"""Check permission within the current organization context."""
def decorator(f):
@wraps(f)
def wrapper(*args, org_id, **kwargs):
user_roles = db.query(
"""SELECT role_id FROM user_roles
WHERE user_id = %s AND scope_type = 'organization' AND scope_id = %s""",
(g.user.id, org_id)
)
role_ids = [r.role_id for r in user_roles]
if not role_ids:
abort(403)
has_permission = db.query_one(
"""SELECT EXISTS(
SELECT 1 FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
WHERE rp.role_id = ANY(%s)
AND p.resource = %s AND p.action = %s
)""",
(role_ids, resource, action)
)
if not has_permission:
abort(403)
return f(*args, org_id=org_id, **kwargs)
return wrapper
return decorator
@app.route('/orgs/<org_id>/billing')
@require_org_permission('billing', 'read')
def view_billing(org_id):
...
Best Practices
- Model permissions as
resource:actionpairs rather than arbitrary strings. This makes the permission model predictable and auditable. - Use role hierarchies to avoid duplicating permission assignments across related roles.
- Scope roles to a context (organization, project, team) rather than making all roles global. This supports multi-tenant applications.
- Cache resolved permissions per session or with a short TTL to avoid repeated database lookups on every request.
- Invalidate the permission cache when roles or assignments change (publish a cache-bust event).
- Audit role assignments and permission checks — log who was granted what, and log denied access attempts.
- Default to deny. If no explicit permission grants access, the request should be rejected.
- Keep the number of roles manageable. If you have more roles than users, your model is likely too granular — consider attribute-based access control (ABAC) instead.
Common Pitfalls
- Checking roles instead of permissions: Code like
if (user.role === 'admin')is brittle. Check permissions so that role definitions can evolve without code changes. - Insufficient granularity: A single
adminrole that grants access to everything violates least privilege. Break it intobilling:admin,users:admin, etc. - No scoping: Global roles mean a user who is an admin of Project A is also admin of Project B. Always scope roles to the relevant context.
- Stale permission caches: After revoking a role, the user retains access until the cache expires. Use event-driven cache invalidation.
- Role explosion: Creating a new role for every unique permission combination. If this happens, consider switching to or supplementing with ABAC.
- Forgetting the default-deny rule: If a new resource is added without corresponding permission checks, it is effectively open to everyone.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add auth-patterns-skills
Related Skills
API Key Auth
API key generation, hashing, rotation, scoping, and rate limiting patterns
JWT Tokens
JWT creation, validation, refresh token rotation, and secure token storage patterns
Multi Tenancy
Multi-tenant authentication isolation, tenant-scoped tokens, and data boundary enforcement
Oauth2 Flows
OAuth 2.0 authorization code, PKCE, client credentials, and token exchange patterns
Passkeys
Passkeys and WebAuthn implementation for passwordless authentication
Session Management
Server-side session handling, cookie security, session stores, and fixation prevention