Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
4.1 KiB
06 — Platform staff roles
Agent: 6/20
Contract: 04-contract.md §3
Status: Implemented (additive schema + middleware + admin APIs + tests)
PROBLEM
Platform access was a single boolean (users.is_platform_admin). Support desk and billing/plan mutation shared the same gate. Contract 04 requires three staff roles with least privilege for support_staff.
Model
| Column | Meaning |
|---|---|
users.is_platform_admin |
Retained. Any platform console access (including support_staff). |
users.staff_role |
admin | developer | support_staff | NULL |
Resolution (ResolveStaffRole / ResolveStaffAccess in apps/api/internal/auth/staff.go):
- Inactive → none
staff_roleif set- Else
is_platform_admin→ treat asadmin(legacy back-compat) - Else none
Capabilities:
| Role | Full /api/admin/* |
Support desk | Notes |
|---|---|---|---|
admin |
yes | yes | Full console |
developer |
yes | yes | Same as admin; env-gated dev tools unchanged |
support_staff |
no | yes | Tickets only — no plans/billing/settings/credits |
legacy is_platform_admin + NULL role |
yes | yes | Migrated admins |
Invariant: assigning a non-empty staff_role sets is_platform_admin=true. Clearing role clears both.
Schema
Migration: apps/api/sql/schema/029_staff_roles.sql
- Additive
staff_roleCHECK + partial index - Idempotent backfill:
staff_role='admin'whereis_platform_adminand role NULL - Migrator
applyPlatformAdminsalsoCOALESCE(staff_role, 'admin')
Middleware (extends existing — no parallel auth)
| Middleware | Allows |
|---|---|
RequirePlatformAdmin |
FullAdmin (admin/developer/legacy) — excludes support_staff |
RequireSupportDesk |
SupportDesk (admin/developer/support_staff/legacy) |
IsPlatformAdmin / checkPlatformAdmin |
Now resolves via GetStaffAccess().FullAdmin |
Router (server.go):
/api/admin/support/tickets*→RequireSupportDesk- All other
/api/admin/*→RequirePlatformAdmin - Staff assign APIs sit under full admin group
StaffRoleAllowsAdminRoute: support_staff → /admin/support* only (contract 04).
APIs (admin | developer only)
| Method | Path | Body | Behavior |
|---|---|---|---|
GET |
/api/admin/staff |
— | List users with staff access |
PATCH |
/api/admin/users/{id}/staff-role |
{"staff_role":"admin"|"developer"|"support_staff"|null} |
Assign/clear; cannot change own role |
GET |
/api/admin/users |
— | Includes staff_role |
GET |
/api/auth/me |
— | Additive staff_access, staff_capabilities when staff |
Errors: 400 invalid role, 403 self-change / capability, 404 unknown user. CSRF still required on mutating admin routes.
Support convenience (also full-admin): PUT /api/admin/support/agents/{id} grants/revokes support_staff only (does not demote admin/developer).
Tests
internal/auth:TestResolveStaffAccess,TestNormalizeStaffRole,TestStaffCapabilities,TestStaffRoleAllowsAdminRouteContractinternal/httpapi:TestRequirePlatformAdmin*(incl. support_staff forbidden),TestRequireSupportDesk,TestHandleAdminSetStaffRoleRejectsSelf
go test ./internal/auth/ ./internal/httpapi/ -count=1
Files touched
apps/api/sql/schema/029_staff_roles.sqlapps/api/internal/auth/staff.go,staff_test.go,staff_role_defaults.go,service.go,invites.goapps/api/internal/httpapi/middleware.go(existing Require*),server.go,admin_staff_handlers.go,admin_handlers.go,auth_handlers.go,admin_authz_test.goapps/api/cmd/migrator/admins.goapps/api/internal/support/agents.go(platform_admin invariant on grant/revoke)
Out of scope / follow-ups
- AdminNav UI filter (agent 7/10/14) — consume
staff_access/staff_capabilitiesfrom/me - Ticket queue assignee filtering (agent 12/14)
- Do not conflate with company
memberships.roleor LLM AI roles