name: implementing-authorization description: Implements permission decisions in backend services — RBAC/ABAC models, resource-ownership checks, IDOR prevention, multi-tenant isolation, and centralized policy enforcement. Use when the user asks to add roles, permissions, access control, admin-only routes, ownership checks, tenant isolation, or asks why a user can see another user's data. Do not use for identity verification and login flows (implementing-authentication) or OS and file-system permissions.
Implementing Authorization
When to use / when NOT to use
- Use for: deciding what an authenticated caller may do — roles, permissions, ownership checks, tenant isolation, policy middleware, privileged-action auditing.
- Do NOT use for: logging users in (implementing-authentication), database GRANTs (db-skills/securing-databases), or OS/file permissions.
Core rules
- Authorize on every request, server-side. Client-side hiding of buttons is UX, not security.
- Deny by default. Routes without an explicit policy are rejected, not allowed.
- ✅
@require(perm="reports:read")on each route; unannotated routes 403 in middleware - ❌ "Only add checks to the sensitive endpoints"
- ✅
- RBAC is the default model (user → roles → permissions). Reach for ABAC (attribute rules) only when decisions depend on resource attributes (owner, status, amount thresholds).
- Authentication is not authorization — check ownership. Fetching by ID must scope to the caller.
- ✅
SELECT ... WHERE id = :id AND owner_id = :caller - ❌
SELECT ... WHERE id = :idafter login — classic IDOR
- ✅
- Multi-tenant: scope every query by
tenant_id, derived from the session/token — never from the request body or URL. Use Postgres row-level security as a backstop where available. - Centralize policy in one module/middleware. Scattered
if user.role == "admin"checks drift and rot; route handlers callauthorize(caller, action, resource)and nothing else. - Audit privileged actions (role grants, data exports, deletions): who, what, when, from where — to an append-only log.
Workflow
- Enumerate actions and resources; write the permission matrix (roles × actions) before code.
- Implement the central
authorize()+ deny-by-default middleware (rule 2, 6). - Add ownership/tenant scoping at the data layer (rules 4–5) so a missed route check cannot leak cross-tenant data.
- Wire the audit log for privileged actions (rule 7).
- Validate: as user A, request user B's resource by ID (expect 403/404); as a role without the permission, call each privileged route (expect 403); confirm an unannotated test route is rejected by default. All three must pass before shipping.
Edge cases & failure modes
- 403 vs 404: return 404 for resources the caller must not know exist (cross-tenant); 403 within a tenant where existence is not secret. Pick per resource and stay consistent.
- Role changes mid-session → re-read roles per request (or short cache ≤60 s); revoke sessions on demotion.
- Background jobs and internal services → they get their own scoped identities, never a shared "system = superuser" that skips
authorize(). - Batch endpoints → authorize each item, not just the endpoint; report per-item denials.
References
Copy-paste patterns and gotchas: see references/patterns.md.