Best Practices

AI Rules for Authorization and RBAC

AI hardcodes if (user.role === 'admin') checks everywhere. Rules for role-based access control, permission hierarchies, attribute-based policies, and middleware-enforced authorization.

8 min read·January 22, 2025

if (user.role === 'admin') scattered across 30 route handlers — miss one and it is a vulnerability

RBAC permission mapping, role hierarchies, ABAC policies, middleware enforcement, audit logging

AI Authorizes with Scattered If-Statements

AI generates authorization with: hardcoded role checks (if (user.role === 'admin')), scattered across every route handler (duplicated in 50 places), no permission granularity (admin can do everything, user can do nothing), no role hierarchy (editor does not inherit viewer permissions), and no audit trail (no log of who accessed what). Every one of these patterns makes authorization fragile, inconsistent, and impossible to audit.

Modern authorization is: centralized (one policy engine, checked via middleware), granular (permissions like 'articles:edit' instead of role strings), hierarchical (admin inherits editor inherits viewer), attribute-aware (ABAC: user.department === resource.department), and audited (every access decision logged). AI generates none of these.

These rules cover: RBAC with permission mapping, role hierarchies, ABAC for contextual access, middleware enforcement, and authorization audit logging.

Rule 1: RBAC with Granular Permission Mapping

The rule: 'Map roles to permissions, not to actions. Define permissions as resource:action pairs: articles:read, articles:create, articles:edit, articles:delete. Map roles to permission sets: const roles = { viewer: ["articles:read"], editor: ["articles:read", "articles:create", "articles:edit"], admin: ["articles:read", "articles:create", "articles:edit", "articles:delete"] }. Check permissions, not roles: hasPermission(user, "articles:edit") — never if (user.role === "admin").'

For the permission check: 'function hasPermission(user: User, permission: string): boolean { const userPermissions = roles[user.role] ?? []; return userPermissions.includes(permission); }. This function is the single source of truth for authorization. When you add a new role (moderator), you add one entry to the roles map — every permission check in the app automatically respects it. No scattered if-statements to update.'

AI generates: if (user.role === 'admin' || user.role === 'editor') — repeated in 30 route handlers. When you add a 'moderator' role, you must find and update all 30 checks. Miss one, and moderators are locked out. With permission mapping, you add 'moderator' to the roles map once, assign permissions, and every hasPermission check works immediately.

  • Permissions as resource:action — articles:read, articles:edit, users:delete
  • Roles map to permission arrays — one definition per role
  • hasPermission(user, 'articles:edit') — never if (user.role === 'admin')
  • New roles: one map entry, zero code changes in route handlers
  • Permission check is the single source of truth — centralized, testable
💡 One Map, Zero Scattered Checks

Adding a 'moderator' role with if-statements means finding and updating 30 route handlers. With permission mapping, you add one entry to the roles map — every hasPermission check in the app respects it immediately. Zero code changes in route handlers.

Rule 2: Role Hierarchies and Inheritance

The rule: 'Define role hierarchies so higher roles inherit lower role permissions. Hierarchy: admin > editor > viewer. Admin gets all editor permissions, editor gets all viewer permissions. Implementation: const hierarchy: Record<string, string[]> = { admin: ["editor"], editor: ["viewer"], viewer: [] }. Resolve permissions by walking the hierarchy: getAllPermissions(role) returns the role permissions plus all inherited permissions.'

For hierarchy resolution: 'function getAllPermissions(role: string): string[] { const direct = roles[role] ?? []; const inherited = (hierarchy[role] ?? []).flatMap(parent => getAllPermissions(parent)); return [...new Set([...direct, ...inherited])]; }. This recursive resolution means: admin inherits from editor which inherits from viewer. You define each role delta — only what it adds — not the full permission list.'

AI generates flat roles with fully duplicated permission lists. Adding a new permission to 'viewer' requires adding it to editor and admin too — three places, easy to miss. With hierarchy inheritance, adding a viewer permission automatically cascades to editor and admin. One change, zero drift.

Rule 3: ABAC for Contextual Access Decisions

The rule: 'When access depends on context — not just role — use Attribute-Based Access Control. ABAC evaluates attributes of the user, resource, and environment: user.department === resource.department (users can only access their department data), user.id === resource.authorId (users can only edit their own content), resource.status === "draft" (only drafts are editable), environment.time within business hours (sensitive operations restricted to work hours). RBAC says what you can do. ABAC adds when, where, and on what.'

For policy definition: 'Define policies as functions: type Policy = (user: User, resource: Resource) => boolean. Example: const canEditArticle: Policy = (user, article) => hasPermission(user, "articles:edit") && (user.id === article.authorId || hasPermission(user, "articles:edit-any")). This combines RBAC (hasPermission) with ABAC (ownership check). Regular editors can edit their own articles; admin editors can edit anyone articles.'

AI generates: if (user.role === 'admin') return true; if (user.id === article.authorId) return true; — two separate checks with no abstraction. When you add a third condition (article.status === 'draft'), you add another if-statement. Policies compose these conditions into a single, testable, reusable function.

⚠️ RBAC Is Not Enough

RBAC says editors can edit articles. But which articles? Their own? Their department? All? ABAC adds context: user.id === article.authorId, user.department === article.department. Without ABAC, every editor can edit every article — probably not what you want.

Rule 4: Middleware-Enforced Authorization

The rule: 'Enforce authorization in middleware — not in route handlers. Middleware ensures every request is checked before reaching business logic. Missing a check in a route handler is a vulnerability; middleware makes it impossible to skip. Pattern: app.use("/api/articles/:id", authorize("articles:edit")), where authorize returns 403 if the user lacks the permission.'

For the authorize middleware: 'function authorize(permission: string) { return (req, res, next) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); if (!hasPermission(req.user, permission)) return res.status(403).json({ error: "Forbidden" }); next(); }; }. 401 = not authenticated (who are you?). 403 = not authorized (you are not allowed). This distinction matters for client-side error handling and security auditing.'

AI puts authorization checks inside route handlers: app.get('/api/articles', (req, res) => { if (req.user.role !== 'admin') return res.status(403)... }). One forgotten check in one route = a security hole. Middleware makes authorization declarative and impossible to bypass — the check runs before your code does.

  • Middleware-first: authorize('articles:edit') before the route handler
  • 401 Unauthorized (not authenticated) vs 403 Forbidden (not authorized)
  • Declarative route protection — impossible to forget a check
  • Combine with ABAC: authorize('articles:edit', { ownerOnly: true })
  • Fail closed: deny by default, allow by explicit permission
ℹ️ Middleware = Impossible to Forget

Authorization in route handlers means one missed check = one security hole. Middleware makes the check declarative: authorize('articles:edit') runs before your code. You cannot reach the route handler without passing the permission check.

Rule 5: Authorization Audit Logging

The rule: 'Log every authorization decision — both grants and denials. Include: who (user ID), what (resource + action), when (timestamp), result (allowed/denied), and why (which permission or policy matched). This audit trail is required for compliance (SOC 2, HIPAA, GDPR) and essential for security incident investigation.'

For structured logging: 'Log format: { event: "authorization", userId: user.id, action: "articles:delete", resourceId: article.id, result: "denied", reason: "missing permission articles:delete", timestamp: new Date().toISOString() }. Structured JSON logs (not console.log strings) enable: filtering by user, searching by action, alerting on repeated denials (possible attack), and compliance reporting.'

AI generates no authorization logging. When a security incident occurs — someone accessed data they should not have — there is no trail. Who accessed what, when, and whether it was authorized: unknowable. Structured audit logs answer these questions in seconds instead of requiring a forensic investigation of application logs.

Complete Authorization Rules Template

Consolidated rules for authorization and RBAC.

  • Permissions as resource:action pairs — never hardcode role strings in route handlers
  • Role-to-permission mapping — one map, centralized, testable
  • Role hierarchy with inheritance — admin > editor > viewer, define deltas not duplicates
  • ABAC for contextual access — ownership, department, status, time-based policies
  • Middleware enforcement — authorize() before route handler, fail closed by default
  • 401 vs 403 — authentication failure vs authorization failure, distinct handling
  • Audit every decision — who, what, when, result, why — structured JSON logs
  • Least privilege default — deny all, allow by explicit permission grant