RBAC (Role-Based Access Control)

Authorization Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Authorization model where permissions are assigned to roles, and roles are assigned to users. Users inherit all permissions from their assigned roles. Simplifies permission management by grouping related permissions into meaningful job functions.

Example

Role “Editor” has permissions [read:articles, write:articles, publish:articles]. Role “Viewer” has [read:articles]. User Alice assigned “Editor” role inherits all three permissions. User Bob assigned “Viewer” can only read.

Analogy

Like job titles in a company. A “Manager” role has certain responsibilities and access (approve expenses, view salaries). Anyone hired as Manager automatically gets those privileges. When they leave that role, they lose them. Much easier than tracking individual permissions for each employee.

Code Example


// RBAC Implementation
class RBACManager {
  constructor() {
    // Define roles and their permissions
    this.roles = {
      'admin': {
        permissions: ['*:*'], // Superuser
        description: 'Full system access'
      },
      'editor': {
        permissions: [
          'read:articles',
          'write:articles',
          'publish:articles',
          'read:comments',
          'moderate:comments'
        ],
        description: 'Content editor'
      },
      'author': {
        permissions: [
          'read:articles',
          'write:own-articles',
          'read:comments'
        ],
        description: 'Content author'
      },
      'viewer': {
        permissions: [
          'read:articles',
          'read:comments'
        ],
        description: 'Read-only access'
      },
      'moderator': {
        permissions: [
          'read:*',
          'delete:comments',
          'ban:users'
        ],
        description: 'Community moderator'
      }
    }

    // Role hierarchy (optional)
    this.hierarchy = {
      'admin': ['editor', 'moderator', 'author', 'viewer'],
      'editor': ['author', 'viewer'],
      'author': ['viewer']
    }
  }

  // Check if user has required role
  hasRole(user, requiredRole) {
    if (!user.roles) return false
    return user.roles.includes(requiredRole)
  }

  // Check if user has any of required roles
  hasAnyRole(user, requiredRoles) {
    if (!user.roles) return false
    return requiredRoles.some(role => user.roles.includes(role))
  }

  // Get all permissions for a user based on their roles
  getUserPermissions(user) {
    const permissions = new Set()

    if (!user.roles) return permissions

    user.roles.forEach(roleName => {
      const role = this.roles[roleName]
      if (role) {
        role.permissions.forEach(perm => permissions.add(perm))
      }

      // Include inherited permissions via hierarchy
      const inheritedRoles = this.hierarchy[roleName] || []
      inheritedRoles.forEach(inheritedRole => {
        const inherited = this.roles[inheritedRole]
        if (inherited) {
          inherited.permissions.forEach(perm => permissions.add(perm))
        }
      })
    })

    return permissions
  }

  // Check if user has specific permission via their roles
  hasPermission(user, requiredPermission) {
    const userPermissions = this.getUserPermissions(user)

    // Check direct match
    if (userPermissions.has(requiredPermission)) {
      return true
    }

    // Check wildcard permissions
    const [action, resource] = requiredPermission.split(':')

    return (
      userPermissions.has('*:*') ||
      userPermissions.has(`${action}:*`) ||
      userPermissions.has(`*:${resource}`)
    )
  }

  // Assign role to user (with validation)
  assignRole(user, roleName) {
    if (!this.roles[roleName]) {
      throw new Error(`Invalid role: ${roleName}`)
    }

    if (!user.roles) {
      user.roles = []
    }

    if (!user.roles.includes(roleName)) {
      user.roles.push(roleName)
    }
  }

  // Remove role from user
  removeRole(user, roleName) {
    if (user.roles) {
      user.roles = user.roles.filter(role => role !== roleName)
    }
  }
}

// Middleware for role-based authorization
const rbac = new RBACManager()

function requireRole(roleName) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    if (!rbac.hasRole(req.user, roleName)) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Required role: ${roleName}`
      })
    }

    next()
  }
}

function requireAnyRole(...roleNames) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    if (!rbac.hasAnyRole(req.user, roleNames)) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Required one of: ${roleNames.join(', ')}`
      })
    }

    next()
  }
}

// Usage in routes
app.get('/api/articles',
  requireAnyRole('viewer', 'author', 'editor', 'admin'),
  async (req, res) => {
    const articles = await db.articles.findAll()
    res.json(articles)
  }
)

app.post('/api/articles',
  requireRole('editor'),
  async (req, res) => {
    const article = await db.articles.create(req.body)
    res.json(article)
  }
)

app.delete('/api/users/:id',
  requireRole('admin'),
  async (req, res) => {
    await db.users.delete(req.params.id)
    res.status(204).send()
  }
)

// Dynamic RBAC with database
class DynamicRBAC {
  async getRolePermissions(roleName) {
    // Fetch from database instead of hardcoded
    const role = await db.roles.findOne({ where: { name: roleName }})
    return role?.permissions || []
  }

  async createRole(name, permissions, description) {
    return await db.roles.create({
      name,
      permissions,
      description,
      createdAt: new Date()
    })
  }

  async updateRolePermissions(roleName, permissions) {
    await db.roles.update(
      { permissions },
      { where: { name: roleName }}
    )

    // Invalidate permission cache
    await this.invalidatePermissionCache(roleName)
  }

  async assignRoleToUser(userId, roleName) {
    await db.userRoles.create({
      userId,
      roleName,
      assignedAt: new Date()
    })

    // Audit log
    await auditLog.log({
      action: 'role_assigned',
      userId,
      roleName,
      timestamp: new Date()
    })
  }
}

Security Notes

SECURITY NOTES

CRITICAL: RBAC (Role-Based Access Control) assigns permissions via roles. Simple but less flexible than ABAC.

RBAC Components:

  • Roles: Groups of permissions (admin, user, guest)
  • Users: Assigned to roles
  • Permissions: Specific capabilities per role
  • Resource: What can be accessed

Implementation:

  • Assign roles: Assign users to roles
  • Role inheritance: Roles can inherit from other roles
  • Dynamic roles: Roles assigned based on context
  • Multiple roles: Users can have multiple roles

Security:

  • Role separation: Prevent conflicting roles
  • Audit: Log role assignments
  • Review: Regularly review role assignments
  • Least privilege: Minimal permissions per role

Advantages:

  • Simple: Easy to understand and implement
  • Scalable: Easy to manage large user bases
  • Auditable: Clear roles for compliance

Limitations:

  • Inflexible: Cannot express complex rules
  • Coarse-grained: Cannot control individual fields
  • Static: Roles defined upfront
  • For context: Cannot consider context (time, location)

Standards & RFCs

Standards & RFCs