Authentication & Authorization
04 / 04

Authorization: RBAC, ABAC & Best Practices

Authorization: RBAC, ABAC & Best Practices

Role-Based Access Control (RBAC)

RBAC assigns permissions to roles, then roles to users. Simple to manage for most applications.

// Roles and permissions
const ROLES = {
  admin:     ['read', 'write', 'delete', 'manage_users'],
  editor:    ['read', 'write'],
  viewer:    ['read'],
  moderator: ['read', 'write', 'delete_comments'],
} as const

// Simple RBAC check
function can(user: User, action: string): boolean {
  const permissions = ROLES[user.role] ?? []
  return permissions.includes(action)
}

// Hierarchical RBAC — roles inherit from parent roles
const ROLE_HIERARCHY = {
  admin: ['editor', 'viewer'],
  editor: ['viewer'],
  viewer: [],
}

// Resource-level RBAC — can user X do action Y on resource Z?
async function authorize(user: User, action: string, resource: Resource): Promise<boolean> {
  if (user.role === 'admin') return true
  if (action === 'read' && resource.isPublic) return true
  if (action === 'write' && resource.ownerId === user.id) return true
  return false
}

// Middleware example (Express)
function requireRole(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!roles.includes(req.user.role))
      return res.status(403).json({ error: 'Forbidden' })
    next()
  }
}

app.delete('/api/users/:id', requireRole('admin'), deleteUser)

Attribute-Based Access Control (ABAC)

ABAC evaluates policies based on attributes of the user, resource, and environment. More flexible than RBAC for complex rules.

// Policy-based authorization
interface AuthContext {
  user: { id: string; role: string; department: string; clearanceLevel: number }
  resource: { ownerId: string; classification: string; department: string }
  environment: { time: Date; ipAddress: string }
}

// Policies are pure functions — easy to test
const policies = {
  'document:read': (ctx: AuthContext) =>
    ctx.resource.classification === 'public' ||
    ctx.user.department === ctx.resource.department ||
    ctx.user.clearanceLevel >= 3,

  'document:delete': (ctx: AuthContext) =>
    ctx.user.role === 'admin' ||
    (ctx.user.id === ctx.resource.ownerId && ctx.user.department === ctx.resource.department),

  'system:access': (ctx: AuthContext) =>
    ctx.environment.time.getHours() >= 9 &&
    ctx.environment.time.getHours() < 18,  // business hours only
}

function authorize(action: string, ctx: AuthContext): boolean {
  return policies[action]?.(ctx) ?? false
}

// Open Policy Agent (OPA) — external policy engine
// Rego policy language; decouples policy from application code

Row-Level Security

// Always filter data by ownership — never return all rows to non-admins
async function getPosts(userId: string, isAdmin: boolean) {
  if (isAdmin) return db.posts.findMany()                  // admin sees all
  return db.posts.findMany({ where: { authorId: userId }}) // user sees own
}

// PostgreSQL Row Level Security (RLS) — enforce at DB level
// CREATE POLICY user_isolation ON posts
//   USING (author_id = current_user_id());
// ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

// Supabase uses RLS extensively
// CREATE POLICY "Users can view own posts" ON posts
//   FOR SELECT USING (auth.uid() = user_id);

Best Practices

  • Principle of least privilege: grant minimum permissions needed. Default deny — explicitly grant access.

  • Check authorization on every request: don't rely on UI hiding. Always enforce on the server.

  • BOLA / IDOR: verify object ownership on every resource endpoint. GET /orders/123 must verify order 123 belongs to the requesting user.

  • Centralize authorization logic: don't scatter if (user.role === "admin") checks — use a policy/guard layer.

  • Audit log: log all authorization failures and sensitive actions (who, what, when, from where).

  • Separate AuthN and AuthZ: authentication verifies identity; authorization grants permissions. Keep the code separate.

  • Re-verify on sensitive operations: even with a valid session, require re-authentication for password change, payment, account deletion.

  • Test authorization: explicitly test that non-owners, non-admins, and unauthenticated users are denied. Happy path tests alone are not enough.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free