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
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 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
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.