36 lines
1.5 KiB
Markdown
36 lines
1.5 KiB
Markdown
# ADR-0007: RBAC permissions instead of role checks
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-08-11
|
|
|
|
## Context
|
|
|
|
`if (user.role === 'ADMIN')` spreads. Six months later authorization logic is scattered
|
|
across dozens of files, no one can answer "who can refund an order?" without grepping, and
|
|
adding a "Warehouse Supervisor" role means editing and redeploying application code.
|
|
|
|
## Decision
|
|
|
|
Authorization is expressed only as permissions (`product.update`, `order.refund`),
|
|
declared on routes with `@RequirePermissions(...)` and evaluated by a global `PermissionsGuard`.
|
|
|
|
Permissions are code: the catalog in `@sport/types` is the source of truth, and the seed
|
|
reconciles the database against it. Roles are data: rows in `roles`/`role_permissions` that a
|
|
SUPER_ADMIN edits at runtime with no deploy.
|
|
|
|
The admin sidebar is built from the same catalog, so a user never sees a link to a screen they
|
|
cannot use — presentation only; the API re-checks every request.
|
|
|
|
## Consequences
|
|
|
|
Every authorization rule is one greppable decorator. New roles need no code. The
|
|
permission set travels inside the access token, so guards do no database work on the hot path —
|
|
which is precisely why access tokens are short-lived (ADR-0008): a revoked permission takes at
|
|
most one token lifetime to take effect.
|
|
|
|
## Alternatives considered
|
|
|
|
Role checks in code — rejected above. Full ABAC/policy engine — rejected as
|
|
premature: nothing yet needs "can edit orders from their own store only". The permission model
|
|
can grow into that if a real requirement appears.
|