ADR 0005 — RBAC: canonical roles, scopes, and permissions
Updates
- 2026-05-15: Added canonical role
student(linked child login viastudents.child_login_user_id); baseline permissions mirror parent minus enrollment; student rows are filtered inGET /api/studentsbychild_login_user_id.
Status
Accepted — 2026-05-15
Context
The MVP used four Dutch role strings on users.role (overheid, school, ouder, chauffeur) as the only authorization signal. Government-side operators need finer separation (e.g. read-only viewers), school/fleet operators need scoped access, and feedback flows must allow POST actions for viewers without granting domain writes.
Decision
- Canonical English roles stored in
users.role:government,government_operator,government_viewer,school,school_operator,fleet_operator,parent,student,driver. - Permissions as string keys in
user_permissions(user_id,permission); handlers useAuthzContext.has(permission)(withgovernmenttreated as super-admin in code). - Scopes as rows in
user_scopes(user_id,scope_type,scope_idwith''when no id):global,government_readonly,schools_all,school_single,fleet_all,fleet_school,own_children,own_bus. - Migration:
ensureSchema-time helpermigrateLegacyRbacmaps legacy Dutch roles to canonical roles, then seeds empty permission/scope tables from role baselines (idempotent per user). - Feedback exception:
government_viewerreceives read-style domain permissions plusfeedback_support_create,feedback_feature_create,feedback_vote; GitHub sync and merge remain behindfeedback_github_sync/feedback_feature_merge/feedback_support_read_allas implemented inserver.tsandsrc/feedback/FeedbackPages.tsx. - Socket.IO: fleet operator room uses
fleet:operators(no Dutch role name in room identifiers).
Consequences
- All new routes must check permissions + scope (not
user.rolealone) for security-sensitive behavior. - UI visibility follows
/api/mepermissions/scopesreturned fromuserJsonForClient. - Caching: at 40M+ users, permission/scope reads should move behind a cache (e.g. Redis) with clear invalidation on
replaceUserAuthz; first implementation loads from Postgres once per request.
References
- Implementation:
lib/authz.ts,server.ts(attachAuthz,migrateLegacyRbac),seedIraqDemo.ts,src/authzClient.ts,src/App.tsx.