---
# === IDENTITY ===
id: software/patterns/rbac-implementation/2026
canonical_question: "How do I implement Role-Based Access Control (RBAC)?"
aliases:
  - "RBAC implementation guide"
  - "role based access control pattern"
  - "how to add roles and permissions"
  - "RBAC database schema design"
  - "permission middleware pattern"
  - "NIST RBAC model implementation"
  - "roles permissions authorization"
  - "RBAC vs ABAC comparison"
entity_type: software_reference
domain: software > patterns > rbac_implementation
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.90
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Always enforce authorization server-side; never rely on client-side role checks alone"
  - "Default to deny — every route/resource must explicitly grant access, not explicitly deny"
  - "Separate authentication from authorization — verify identity first, then check permissions"
  - "Check permissions not roles — code should test 'can user do X?' not 'is user admin?'"
  - "Principle of least privilege — assign the minimum permissions required for each role"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "You need attribute-based fine-grained access (time-of-day, IP, resource owner)"
    use_instead: "ABAC or ReBAC pattern (not yet in library)"
  - condition: "You need object-level ownership checks (user can only edit their own posts)"
    use_instead: "Relationship-Based Access Control (ReBAC) or combine RBAC with ownership checks"

# === AGENT HINTS ===
inputs_needed:
  - key: framework
    question: "What web framework are you using?"
    type: choice
    options: ["Express/Node.js", "Django/Python", "Spring Boot/Java", "Go net/http", "Rails", "ASP.NET", "Other"]
  - key: rbac_model
    question: "Which RBAC model complexity do you need?"
    type: choice
    options: ["flat (RBAC0 - roles with direct permissions)", "hierarchical (RBAC1 - roles inherit from parent roles)", "constrained (RBAC2 - mutual exclusion, cardinality limits)"]
  - key: storage
    question: "Where will you store roles and permissions?"
    type: choice
    options: ["relational database (PostgreSQL, MySQL)", "policy engine (Casbin, OPA, Cedar)", "identity provider (Auth0, Okta, Cognito)", "in-memory/config file"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/rbac-implementation/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: software/patterns/jwt-implementation/2026
      label: "JWT Implementation"
    - id: software/patterns/session-management/2026
      label: "Session Management"
    - id: software/patterns/mfa-implementation/2026
      label: "Multi-Factor Authentication"
    - id: software/patterns/api-key-management/2026
      label: "API Key Management"
  solves: []
  alternative_to: []
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "Role-Based Access Control (RBAC) — NIST CSRC"
    author: NIST
    url: https://csrc.nist.gov/projects/role-based-access-control
    type: official_docs
    published: 2020-10-01
    reliability: authoritative
  - id: src2
    title: "Authorization Cheat Sheet — OWASP"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: high
  - id: src3
    title: "RBAC Documentation — Apache Casbin"
    author: Casbin
    url: https://casbin.org/docs/rbac/
    type: official_docs
    published: 2025-01-10
    reliability: high
  - id: src4
    title: "Security Best Practices in IAM — AWS"
    author: AWS
    url: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html
    type: official_docs
    published: 2025-11-01
    reliability: high
  - id: src5
    title: "Access Control Cheat Sheet — OWASP"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: high
  - id: src6
    title: "Implement Access Control — OWASP Top 10 Proactive Controls"
    author: OWASP
    url: https://top10proactive.owasp.org/the-top-10/c1-accesscontrol/
    type: community_resource
    published: 2024-09-01
    reliability: high
  - id: src7
    title: "A Proposed Standard for Role-Based Access Control — Ferraiolo, Sandhu, Gavrila, Kuhn"
    author: NIST
    url: https://csrc.nist.gov/csrc/media/projects/role-based-access-control/documents/rbac-std-draft.pdf
    type: rfc_spec
    published: 2001-10-01
    reliability: authoritative
---

# Role-Based Access Control (RBAC): Complete Reference

## TL;DR

- **Bottom line**: Assign permissions to roles (not directly to users), then assign users to roles — this decouples who someone is from what they can do.
- **Key tool/command**: `authorize(user, permission, resource)` middleware/decorator on every protected route.
- **Watch out for**: Role explosion — creating a new role for every edge case instead of composing permissions.
- **Works with**: Any language/framework. Database-backed (PostgreSQL, MySQL) or policy-engine-backed (Casbin, OPA, Cedar, AWS IAM).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Always enforce authorization server-side; never rely on client-side role checks alone [src2]
- Default to deny — every route/resource must explicitly grant access, not explicitly deny [src2]
- Separate authentication from authorization — verify identity first, then check permissions [src1]
- Check permissions not roles — code should test "can user do X?" not "is user admin?" [src6]
- Principle of least privilege — assign the minimum permissions required for each role [src4]
- Log all authorization decisions (grant and deny) for audit trails [src2]

## Quick Reference

| RBAC Model | Formal Name | Hierarchy | Constraints | Best For | Complexity |
|---|---|---|---|---|---|
| RBAC0 (flat) | Core RBAC | None | None | Small apps, <10 roles | Low |
| RBAC1 (hierarchical) | Hierarchical RBAC | Roles inherit from parent roles | None | Enterprise apps, department trees | Medium |
| RBAC2 (constrained) | Constrained RBAC | None | Mutual exclusion, cardinality limits | Compliance-heavy (finance, healthcare) | High |
| RBAC3 (symmetric) | RBAC1 + RBAC2 | Full hierarchy | Full constraints | Large enterprise with SOX/HIPAA | High |
| ABAC | Attribute-Based | N/A — uses attributes | Attribute policies | Fine-grained, context-dependent | Very High |
| ReBAC | Relationship-Based | Object graph | Relationship tuples | Social apps, document sharing (Google Drive model) | High |
| ACL | Access Control List | N/A — per-object | Per-resource lists | File systems, simple object-level control | Low-Medium |

| Component | Purpose | Storage Options |
|---|---|---|
| Users | Authenticated identities | Identity provider, users table |
| Roles | Named collections of permissions | roles table, policy file |
| Permissions | Actions on resources (read, write, delete) | permissions table, policy engine |
| User-Role mapping | Which users hold which roles | user_roles join table |
| Role-Permission mapping | Which roles grant which permissions | role_permissions join table |
| Sessions | Active role assignments per login | JWT claims, session store |

## Decision Tree

```
START
├── Fewer than 5 roles, no hierarchy needed?
│   ├── YES → Use RBAC0 (flat). DB join tables or simple config. See Step-by-Step below.
│   └── NO ↓
├── Roles have parent-child relationships (manager inherits team-lead)?
│   ├── YES → Use RBAC1 (hierarchical). Add parent_role_id to roles table or use Casbin.
│   └── NO ↓
├── Need mutual exclusion (user cannot be both auditor and treasurer)?
│   ├── YES → Use RBAC2/RBAC3 (constrained). Add constraint rules to role assignment logic.
│   └── NO ↓
├── Need fine-grained context (time-of-day, IP, resource owner)?
│   ├── YES → RBAC alone is insufficient. Add ABAC layer (OPA, Cedar) or combine RBAC + ownership checks.
│   └── NO ↓
├── Want to manage policies externally (not in app code)?
│   ├── YES → Use a policy engine: Casbin (lightweight), OPA (cloud-native), Cedar (AWS).
│   └── NO ↓
└── DEFAULT → Start with RBAC0 in your DB. Add hierarchy later when you hit >10 roles.
```

## Step-by-Step Guide

### 1. Design the database schema

Create the five core tables: users, roles, permissions, user_roles, and role_permissions. This normalized schema prevents permission duplication and enables role reuse. [src1]

```sql
-- Core RBAC schema (PostgreSQL)
CREATE TABLE roles (
  id         SERIAL PRIMARY KEY,
  name       VARCHAR(50) UNIQUE NOT NULL,
  parent_id  INT REFERENCES roles(id),  -- NULL for RBAC0, used for RBAC1
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE permissions (
  id       SERIAL PRIMARY KEY,
  action   VARCHAR(50) NOT NULL,       -- e.g. 'read', 'write', 'delete'
  resource VARCHAR(100) NOT NULL,      -- e.g. 'articles', 'users', 'reports'
  UNIQUE(action, resource)
);

CREATE TABLE role_permissions (
  role_id       INT REFERENCES roles(id) ON DELETE CASCADE,
  permission_id INT REFERENCES permissions(id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id    INT REFERENCES users(id) ON DELETE CASCADE,
  role_id    INT REFERENCES roles(id) ON DELETE CASCADE,
  assigned_at TIMESTAMPTZ DEFAULT now(),
  PRIMARY KEY (user_id, role_id)
);
```

**Verify**: `SELECT * FROM roles;` → should show your seeded roles.

### 2. Seed initial roles and permissions

Insert your baseline roles and their associated permissions. Start with the minimum set — you can always add more. [src4]

```sql
-- Seed roles
INSERT INTO roles (name) VALUES ('viewer'), ('editor'), ('admin');

-- Seed permissions
INSERT INTO permissions (action, resource) VALUES
  ('read',   'articles'),
  ('write',  'articles'),
  ('delete', 'articles'),
  ('read',   'users'),
  ('write',  'users'),
  ('delete', 'users');

-- Assign permissions to roles
-- viewer: read articles
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'viewer' AND p.action = 'read' AND p.resource = 'articles';

-- editor: read + write articles
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'editor' AND p.action IN ('read', 'write') AND p.resource = 'articles';

-- admin: all permissions
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'admin';
```

**Verify**: `SELECT r.name, p.action, p.resource FROM role_permissions rp JOIN roles r ON r.id = rp.role_id JOIN permissions p ON p.id = rp.permission_id ORDER BY r.name;` → should show complete permission matrix.

### 3. Build authorization middleware

Create a reusable middleware/decorator that intercepts every request, extracts the user's roles, resolves their permissions, and checks against the required permission for the route. [src2]

```javascript
// Node.js/Express — permission-checking middleware
async function authorize(requiredAction, requiredResource) {
  return async (req, res, next) => {
    const userId = req.user?.id; // set by authentication middleware
    if (!userId) return res.status(401).json({ error: 'Not authenticated' });

    // Query user's effective permissions through role assignments
    const { rows } = await db.query(`
      SELECT DISTINCT p.action, p.resource
      FROM user_roles ur
      JOIN role_permissions rp ON rp.role_id = ur.role_id
      JOIN permissions p ON p.id = rp.permission_id
      WHERE ur.user_id = $1 AND p.action = $2 AND p.resource = $3
    `, [userId, requiredAction, requiredResource]);

    if (rows.length === 0) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// Usage on routes
app.get('/articles',     authorize('read', 'articles'),  getArticles);
app.post('/articles',    authorize('write', 'articles'), createArticle);
app.delete('/articles/:id', authorize('delete', 'articles'), deleteArticle);
```

**Verify**: Call `GET /articles` with a viewer token → 200. Call `DELETE /articles/1` with viewer token → 403.

### 4. Implement role assignment API

Provide admin-only endpoints to assign and revoke roles. Always check that the assigner has permission to manage roles. [src4]

```javascript
// Assign role to user (admin-only)
app.post('/admin/users/:userId/roles',
  authorize('write', 'users'),
  async (req, res) => {
    const { userId } = req.params;
    const { roleName } = req.body;

    const role = await db.query('SELECT id FROM roles WHERE name = $1', [roleName]);
    if (role.rows.length === 0) return res.status(404).json({ error: 'Role not found' });

    await db.query(
      'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
      [userId, role.rows[0].id]
    );
    res.json({ message: `Role '${roleName}' assigned to user ${userId}` });
  }
);
```

**Verify**: `POST /admin/users/5/roles {"roleName":"editor"}` → 200. Then query `user_roles` to confirm mapping exists.

### 5. Add permission caching (production optimization)

For high-traffic apps, cache the user's resolved permissions to avoid repeated DB joins on every request. Invalidate on role or permission changes. [src4]

```javascript
// Redis-backed permission cache (Node.js)
const CACHE_TTL = 300; // 5 minutes

async function getPermissions(userId) {
  const cacheKey = `perms:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const { rows } = await db.query(`
    SELECT DISTINCT p.action || ':' || p.resource AS perm
    FROM user_roles ur
    JOIN role_permissions rp ON rp.role_id = ur.role_id
    JOIN permissions p ON p.id = rp.permission_id
    WHERE ur.user_id = $1
  `, [userId]);

  const perms = rows.map(r => r.perm);
  await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(perms));
  return perms;
}

// Invalidate when roles change
async function invalidateUserPermissions(userId) {
  await redis.del(`perms:${userId}`);
}
```

**Verify**: Check Redis `GET perms:5` → should show `["read:articles","write:articles"]`. Change role, call invalidate, confirm key is deleted.

## Code Examples

### Node.js/Express: Permission middleware with role hierarchy

```javascript
// Input:  authenticated request with req.user.id
// Output: 403 if denied, next() if allowed

const authorize = (action, resource) => async (req, res, next) => {
  if (!req.user?.id) return res.status(401).json({ error: 'Unauthenticated' });

  // Resolve roles including hierarchy (RBAC1)
  const { rows } = await pool.query(`
    WITH RECURSIVE role_tree AS (
      SELECT r.id FROM user_roles ur JOIN roles r ON r.id = ur.role_id
      WHERE ur.user_id = $1
      UNION ALL
      SELECT r.id FROM roles r JOIN role_tree rt ON r.parent_id = rt.id
    )
    SELECT 1 FROM role_tree rt
    JOIN role_permissions rp ON rp.role_id = rt.id
    JOIN permissions p ON p.id = rp.permission_id
    WHERE p.action = $2 AND p.resource = $3
    LIMIT 1
  `, [req.user.id, action, resource]);

  if (rows.length === 0) return res.status(403).json({ error: 'Forbidden' });
  next();
};
```

### Python/Django: Decorator-based permission check

> Full script: [python-django-decorator-based-permission-check.py](scripts/python-django-decorator-based-permission-check.py) (28 lines)

```python
# Input:  Django request with request.user
# Output: 403 HttpResponseForbidden or proceeds to view
from functools import wraps
from django.http import HttpResponseForbidden
from myapp.models import UserRole, RolePermission
# ... (see full script)
```

### Go: Middleware with Casbin policy engine

> Full script: [go-middleware-with-casbin-policy-engine.go](scripts/go-middleware-with-casbin-policy-engine.go) (31 lines)

```go
// Input:  HTTP request with user ID in context
// Output: 403 if Casbin denies, next handler if allowed
package main
import (
    "net/http"
# ... (see full script)
```

### Java/Spring Security: Method-level authorization

> Full script: [java-spring-security-method-level-authorization.java](scripts/java-spring-security-method-level-authorization.java) (28 lines)

```java
// Input:  Spring Security authenticated principal
// Output: AccessDeniedException if permission missing
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Hardcoding role names in business logic

```javascript
// BAD -- tightly couples code to specific role names
if (user.role === 'admin') {
  allowDelete();
}
// Adding a "super_admin" role requires changing every check
```

### Correct: Check permissions, not role names

```javascript
// GOOD -- checks capability, not identity
if (await hasPermission(user.id, 'delete', 'articles')) {
  allowDelete();
}
// New roles automatically work if they have the permission
```

### Wrong: Client-side only authorization

```javascript
// BAD -- hiding UI elements is not authorization
{user.role === 'admin' && <DeleteButton />}
// Anyone can call the API directly and bypass this
```

### Correct: Server-side enforcement with optional UI hiding

```javascript
// GOOD -- server enforces, client hides for UX only
// Server: authorize('delete', 'articles') middleware on DELETE route
// Client: conditionally render for better UX, but server is source of truth
app.delete('/articles/:id', authorize('delete', 'articles'), deleteHandler);
```

### Wrong: Storing roles in JWT without server-side validation

```javascript
// BAD -- JWT role claim is stale after role revocation
const decoded = jwt.verify(token, secret);
if (decoded.roles.includes('admin')) {
  // User was demoted 5 minutes ago but JWT hasn't expired
  allowAdminAction();
}
```

### Correct: Use JWT for identity, check permissions from DB/cache

```javascript
// GOOD -- JWT identifies user, permissions checked live
const decoded = jwt.verify(token, secret);
const userId = decoded.sub;
const hasAccess = await checkPermission(userId, 'admin_action', 'system');
if (hasAccess) allowAdminAction();
```

### Wrong: One mega-role instead of composable permissions

```sql
-- BAD -- "god role" that grants everything
INSERT INTO roles (name) VALUES ('super_admin_everything');
-- No granularity, violates least privilege
```

### Correct: Compose roles from granular permissions

```sql
-- GOOD -- fine-grained, composable
INSERT INTO roles (name) VALUES ('article_manager'), ('user_manager');
-- article_manager gets read+write+delete on articles
-- user_manager gets read+write on users
-- Super admin = both roles assigned to user
```

## Common Pitfalls

- **Role explosion**: Creating a new role for every permission combination (viewer-articles, viewer-reports, viewer-articles-reports...). Fix: Use composable permissions and assign multiple roles per user. [src2]
- **Missing deny-by-default**: Forgetting to protect new routes/endpoints. Fix: Use a global middleware that requires explicit `authorize()` on every route; reject undecorated routes in tests. [src2]
- **Stale permission cache**: Changing a user's role but the old permissions are cached. Fix: Invalidate cache on any role/permission change using `invalidateUserPermissions(userId)`. [src4]
- **N+1 query on every request**: Querying roles, then permissions, then resources in separate queries. Fix: Use a single JOIN query or pre-cache the permission set. [src5]
- **No audit logging**: Cannot trace who accessed what or when a role was changed. Fix: Log every authorization decision and role assignment change to an append-only audit table. [src2]
- **Hierarchy loops**: In RBAC1, setting role A's parent to B and B's parent to A creates an infinite loop. Fix: Use a recursive CTE with `CYCLE` detection or enforce a max depth (Casbin defaults to 10). [src3]
- **Confusing authentication with authorization**: Checking "is logged in?" instead of "has permission?". Fix: Always separate authn middleware (verify token) from authz middleware (check permission). [src6]
- **Overly broad wildcard permissions**: Granting `*:*` (all actions on all resources) to convenience roles. Fix: Never use wildcards in production; explicitly enumerate permissions. [src4]

## Diagnostic Commands

```bash
# Check a user's effective roles (PostgreSQL)
SELECT r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = 42;

# Check a user's effective permissions (including role hierarchy)
WITH RECURSIVE role_tree AS (
  SELECT r.id, r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = 42
  UNION ALL
  SELECT r.id, r.name FROM roles r JOIN role_tree rt ON r.parent_id = rt.id
)
SELECT DISTINCT p.action, p.resource
FROM role_tree rt
JOIN role_permissions rp ON rp.role_id = rt.id
JOIN permissions p ON p.id = rp.permission_id;

# Casbin: test a permission
casbin-cli enforce --model model.conf --policy policy.csv "alice" "/articles" "GET"

# Find users with a specific permission
SELECT DISTINCT u.id, u.email
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE p.action = 'delete' AND p.resource = 'users';

# Detect orphaned role assignments (role deleted but mapping remains)
SELECT ur.* FROM user_roles ur LEFT JOIN roles r ON r.id = ur.role_id WHERE r.id IS NULL;
```

## Version History & Compatibility

| Standard / Tool | Version | Status | Key Change |
|---|---|---|---|
| NIST RBAC (INCITS 359) | 2012 | Current standard | Defines RBAC0-RBAC3 formally |
| NIST SP 800-53 Rev. 5 | 2020 | Current | AC-3(7) codifies RBAC as access control policy |
| Casbin | v2.x (2024) | Current | RBAC with domains, pattern matching, priority policies |
| Spring Security | 6.x (2024) | Current | `@PreAuthorize` with SpEL, method-level security |
| AWS IAM | 2025 | Current | IAM Access Analyzer auto-detects unused permissions |
| Django | 5.x (2025) | Current | Built-in `Permission` model, `@permission_required` |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Users fit into well-defined job categories | Permissions depend on object ownership (only author can edit their post) | ReBAC (Zanzibar/SpiceDB) or RBAC + ownership filter |
| You have fewer than ~50 distinct roles | You need fine-grained context (time, location, risk score) | ABAC (OPA, Cedar, XACML) |
| Compliance requires auditable role assignments (SOX, HIPAA) | Access is per-document or per-record, not per-resource-type | ACL or ReBAC |
| Multi-tenant with shared role definitions per tenant | Roles change so frequently that management overhead exceeds benefit | ABAC with dynamic attributes |
| You want a simple, well-understood model teams can reason about | You have only 2 roles (logged-in vs. not) | Simple boolean `isAuthenticated` check |

## Important Caveats

- RBAC is a pattern, not a product. Implementation quality varies enormously — a poorly implemented RBAC is worse than no RBAC because it creates a false sense of security. [src2]
- The NIST model (INCITS 359-2012) defines the theoretical framework. Real implementations must also handle session management, token expiry, and cache invalidation — none of which the standard covers. [src1]
- Policy engines (Casbin, OPA, Cedar) add operational complexity. For apps with fewer than 10 roles and straightforward permission logic, database-backed RBAC with middleware is simpler and sufficient. [src3]
- OWASP recommends preferring ABAC or ReBAC over RBAC for new greenfield applications due to superior scalability and fine-grained control. RBAC remains the right choice when simplicity and auditability are primary requirements. [src2]
- In microservice architectures, propagating RBAC decisions across services requires either a centralized policy service or embedding permission claims in tokens (with the caveat of staleness). [src4]

## Related Units

- [JWT Implementation](/software/patterns/jwt-implementation/2026)
- [Session Management](/software/patterns/session-management/2026)
- [Multi-Factor Authentication](/software/patterns/mfa-implementation/2026)
- [API Key Management](/software/patterns/api-key-management/2026)
