What Is RBAC?


Role-Based Access Control (RBAC) is an authorization model where permissions are assigned to roles, and users are assigned to those roles. Instead of managing permissions for each user individually, you manage roles and their associated permissions, then assign users to appropriate roles.


Core RBAC Concepts


| Concept | Definition | Example |

|---------|------------|---------|

| User | An individual account | alice@example.com |

| Role | A collection of permissions | Editor, Admin, Viewer |

| Permission | An action on a resource | article:create, article:delete |

| Resource | The object being accessed | Article, User, Comment |

| Session | A user's active role mapping | Alice as Editor |


The RBAC Model


Level 1: Flat RBAC


Users are assigned roles, and roles have permissions. This simple structure works for most applications.



Users ──< Role Assignment >── Roles ──< Permission Assignment >── Permissions


Level 2: Hierarchical RBAC


Roles can inherit permissions from other roles. For example, an Admin role inherits all Editor permissions plus additional administrative permissions.



Admin ──inherits──> Editor ──inherits──> Viewer


Database Schema



-- Core RBAC tables

CREATE TABLE roles (

    id SERIAL PRIMARY KEY,

    name VARCHAR(50) UNIQUE NOT NULL,

    description TEXT

);



CREATE TABLE permissions (

    id SERIAL PRIMARY KEY,

    resource VARCHAR(50) NOT NULL,

    action VARCHAR(20) NOT NULL,

    UNIQUE(resource, action)

);



CREATE TABLE role_permissions (

    role_id INTEGER REFERENCES roles(id),

    permission_id INTEGER REFERENCES permissions(id),

    PRIMARY KEY (role_id, permission_id)

);



CREATE TABLE user_roles (

    user_id INTEGER REFERENCES users(id),

    role_id INTEGER REFERENCES roles(id),

    granted_by INTEGER REFERENCES users(id),

    granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (user_id, role_id)

);



-- For hierarchical RBAC

CREATE TABLE role_hierarchy (

    parent_role_id INTEGER REFERENCES roles(id),

    child_role_id INTEGER REFERENCES roles(id),

    PRIMARY KEY (parent_role_id, child_role_id)

);


Implementation Patterns


Node.js / Express Middleware



// Permission definition

const PERMISSIONS = {

    ARTICLE: {

        CREATE: 'article:create',

        READ: 'article:read',

        UPDATE: 'article:update',

        DELETE: 'article:delete',

        PUBLISH: 'article:publish'

    },

    USER: {

        LIST: 'user:list',

        MANAGE: 'user:manage'

    }

};



// Role definitions with permission inheritance

const ROLES = {

    viewer: {

        permissions: ['article:read'],

        inherits: []

    },

    editor: {

        permissions: ['article:create', 'article:update'],

        inherits: ['viewer']

    },

    admin: {

        permissions: [

            'article:delete', 'article:publish',

            'user:list', 'user:manage'

        ],

        inherits: ['editor']

    }

};



// Authorization middleware

function authorize(...requiredPermissions) {

    return (req, res, next) => {

        const userRoles = req.user.roles;

        const userPermissions = getUserPermissions(userRoles);



        const hasPermission = requiredPermissions.every(

            p => userPermissions.includes(p)

        );



        if (!hasPermission) {

            return res.status(403).json({

                error: 'Insufficient permissions'

            });

        }

        next();

    };

}



// Usage in routes

app.post('/api/articles',

    authenticate,

    authorize(PERMISSIONS.ARTICLE.CREATE),

    createArticle

);



app.delete('/api/articles/:id',

    authenticate,

    authorize(PERMISSIONS.ARTICLE.DELETE),

    deleteArticle

);


Python / FastAPI



from enum import Enum

from functools import wraps

from fastapi import HTTPException, Depends



class Permission(Enum):

    ARTICLE_CREATE = "article:create"

    ARTICLE_READ = "article:read"

    ARTICLE_UPDATE = "article:update"

    ARTICLE_DELETE = "article:delete"

    USER_MANAGE = "user:manage"



ROLE_PERMISSIONS = {

    "viewer": {Permission.ARTICLE_READ},

    "editor": {

        Permission.ARTICLE_READ,

        Permission.ARTICLE_CREATE,

        Permission.ARTICLE_UPDATE,

    },

    "admin": {

        Permission.ARTICLE_READ,

        Permission.ARTICLE_CREATE,

        Permission.ARTICLE_UPDATE,

        Permission.ARTICLE_DELETE,

        Permission.USER_MANAGE,

    },

}



def require_permission(permission: Permission):

    def decorator(func):

        @wraps(func)

        async def wrapper(*args, **kwargs):

            user = kwargs.get('current_user')

            if not user:

                raise HTTPException(status_code=401)



            user_perms = ROLE_PERMISSIONS.get(user.role, set())

            if permission not in user_perms:

                raise HTTPException(

                    status_code=403,

                    detail="Insufficient permissions"

                )

            return await func(*args, **kwargs)

        return wrapper

    return decorator



# Usage

@app.delete("/articles/{article_id}")

@require_permission(Permission.ARTICLE_DELETE)

async def delete_article(article_id: int, current_user = Depends(get_current_user)):

    # Delete logic

    pass


Attribute-Based Access Control (ABAC)


For more granular control, extend RBAC with ABAC policies that consider resource attributes and context:



def can_access_resource(user, resource, action):

    """Check RBAC first, then ABAC policies."""

    # RBAC check

    if not has_role_permission(user.role, action):

        return False



    # ABAC policies

    policies = {

        # Users can edit their own articles

        ('article:update', 'article'): lambda u, r: r.author_id == u.id,

        # Admins can edit any article

        ('article:update', 'article'): lambda u, r: 'admin' in u.roles,

        # Only article authors can delete

        ('article:delete', 'article'): lambda u, r: r.author_id == u.id,

    }



    policy = policies.get((action, resource.type))

    if policy:

        return policy(user, resource)

    return False


Performance Optimization


Cache resolved permissions to avoid repeated database queries:



import redis

import json



r = redis.Redis()



def get_cached_permissions(user_id):

    cache_key = f"perms:{user_id}"

    cached = r.get(cache_key)

    if cached:

        return json.loads(cached)



    permissions = resolve_user_permissions(user_id)

    r.setex(cache_key, 300, json.dumps(permissions))  # 5 min TTL

    return permissions


Audit Logging


Every authorization decision should be logged:



def log_authorization(user_id, action, resource, granted, reason=""):

    log_entry = {

        "timestamp": datetime.utcnow().isoformat(),

        "user_id": user_id,

        "action": action,

        "resource": resource,

        "granted": granted,

        "reason": reason,

        "source_ip": request.remote_addr

    }

    audit_logger.info(json.dumps(log_entry))


Common Pitfalls


  • **Role explosion**: Too many fine-grained roles become unmanageable. Aim for 5-10 roles maximum.
  • **Hardcoded role checks**: `if user.role === 'admin'` spreads business logic everywhere. Always check permissions, not role names.
  • **Missing negative permissions**: RBAC naturally handles allow rules. For deny rules, evaluate deny before allow.
  • **User in multiple roles**: Decide whether permissions are additive (union) or restrictive (intersection).

  • Summary


    RBAC simplifies authorization by grouping permissions into roles and assigning roles to users. Start with flat RBAC and add hierarchy as needed. Always check permissions rather than role names, cache resolved permissions for performance, and layer on ABAC policies for fine-grained access control. Log all authorization decisions for auditability.