Files
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

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):

  1. Inactive → none
  2. staff_role if set
  3. Else is_platform_admin → treat as admin (legacy back-compat)
  4. 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_role CHECK + partial index
  • Idempotent backfill: staff_role='admin' where is_platform_admin and role NULL
  • Migrator applyPlatformAdmins also COALESCE(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, TestStaffRoleAllowsAdminRouteContract
  • internal/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.sql
  • apps/api/internal/auth/staff.go, staff_test.go, staff_role_defaults.go, service.go, invites.go
  • apps/api/internal/httpapi/middleware.go (existing Require*), server.go, admin_staff_handlers.go, admin_handlers.go, auth_handlers.go, admin_authz_test.go
  • apps/api/cmd/migrator/admins.go
  • apps/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_capabilities from /me
  • Ticket queue assignee filtering (agent 12/14)
  • Do not conflate with company memberships.role or LLM AI roles