# Hardening Phase 1 — Admin RBAC & staff role scopes (make role checks real)

> Resurrect the dead `DynamicPermission` policy so the seeded admin personas can actually operate the
> backoffice, expose the already-built role-management handlers over HTTP (delivering REQ-031), and
> narrow the over-broad `BookingRoles.Admin` bucket that hands Support/Moderation staff clinical and
> financial access. This is the server half of *"roles aren't checked."*
> **Track:** backend · **Depends on:** — (parallel-safe with Phase 0) · **Unlocks:** Phase 4
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**

## 1. Context — where this sits

Fixes **H-04, H-05** from [issues.md](issues.md). Every admin endpoint is gated by
`[Authorize(ConstantPolicies.DynamicPermission)]`, whose handler passes only for the literal role
`"admin"` or a matching RoleClaim — but no user holds `"admin"`, no RoleClaim is ever seeded, and the
only claim-writing service has no controller. Net effect: the seeded `super_admin` (09120000020) and
`finance` (09120000021) personas — which RUNBOOK.md documents as the sanctioned admin path — get 403 on
**every** admin console action. Separately, handler-level role sets are the entire authz boundary for
bookings (controllers carry blanket `[Authorize]`), and `BookingRoles.Admin` bundles five staff roles
into clinical-notes/nurse-balance/forced-transition access, violating the codebase's own
"narrowest fitting scope" convention.

**What already exists (do not rebuild):** the `DynamicPermissionHandler`/`DynamicPermissionService`
wiring (`ServiceCollectionExtension.cs:49-50,100-103`), `RoleManagerService.ChangeRolePermissionsAsync`
(`RoleManagerService.cs:145-208` — the claim writer), the Application-layer
`Features/{Role,Admin}` commands/queries (handlers exist, no HTTP surface), the demo seeder
(`DemoWorldSeeder.cs:279-308`), tenancy checks in handlers (correct — leave alone), and
`Baya.Application/Common/StaffRoles.cs` + `Features/PatientCareRecords/PatientAccess.cs:17` (the
narrow-scope pattern to mirror).

## 2. Required reading (do this first)

- [issues.md](issues.md) H-04/H-05 — evidence with file:line; don't re-audit.
- `DynamicPermissionService.cs`, `RoleNames.cs`, `RoleManagerService.cs`,
  `DemoWorldDefinitions.cs`/`DemoWorldSeeder.cs`, `Features/Bookings/BookingRoles.cs` and the three
  handlers listed in H-05, `Common/StaffRoles.cs`, `PatientAccess.cs`.
- `dev/post-phase/refinement/RUNBOOK.md:115-130` and
  `dev/shared-working-context/reports/refinement-phase-2-report.md` — the docs that currently
  overstate what works (you will correct them).
- `server/CLAUDE.md` + `server/CONVENTIONS.md` (patterns, zero-new-warnings gate).
- REQ-031 in `dev/shared-working-context/frontend/requests/for-backend.md` (the RBAC endpoints the
  frontend's `/admin/roles` grid expects).

## 3. Scope — build this

1. **Make DynamicPermission passable.**
   - `DynamicPermissionService.CanAccess`: treat `RoleNames.SuperAdmin` (and `RoleNames.Admin`) as a
     full bypass — mirroring how the rest of the codebase treats super_admin — instead of the single
     literal `"admin"`.
   - **Seed RoleClaims** for the fine-grained roles (`finance`, `support`, `moderation`) mapping each
     to the controller/action set it should reach (finance → payouts/refunds/invoices/BNPL admin;
     support → tickets/alerts; moderation → reviews queue). Seed via the same idempotent mechanism the
     demo seeder uses (or a migration `HasData` if that's the house style — check how roles themselves
     are seeded). A fresh clone must have a working admin console out of the box.
2. **Expose role management over HTTP (REQ-031).** Add a `RolesController` (or `AdminRolesController`)
   under `Controllers/V1` wiring the existing `Features/Role` handlers: list roles (+ their claims),
   grant/revoke a user role, update role permissions. Gate it to `super_admin` only. Follow the house
   controller conventions (snake_case routes, ApiResult envelope, versioning). Mark REQ-031 delivered
   in `for-backend.md`.
3. **Split `BookingRoles.Admin`.** Introduce purpose-specific sets (suggested:
   `BookingRoles.ClinicalAccess = [Admin, SuperAdmin]`,
   `BookingRoles.Financial = [Admin, SuperAdmin, Finance]`, keep a `Staff` set where genuinely all
   staff belong, e.g. read-only booking lookups for ticket context). Update the users:
   `GetCareInstructionsQuery` (clinical), `TransitionBookingStatusCommand` (financial — it arms the
   payout trigger), `GetNursePayableBalanceQuery` (financial), and review the remaining
   `BookingRoles.Admin` consumers (`CancelSessionCommand`, `CancelBookingCommand`,
   `SubmitCareInstructionsCommand`, `GetVisitVerificationQuery`, `ListBookingsQuery`,
   `GetBookingDetailQuery`, `GetBookingRequestQuery`) — assign each the narrowest set that matches
   what it exposes. Justify each choice in the report.
4. **Docs honesty.** Correct `RUNBOOK.md` + the refinement-phase-2 report claims about the admin
   personas; document the finance/support/moderation scopes somewhere durable (server/CLAUDE.md or a
   product note).

## 4. Mocks & seams in this phase

None. This is authorization wiring only — no vendor seams, no schema beyond possible RoleClaim seed
rows.

## 5. Critical rules you must not get wrong

- **Do not weaken anything:** endpoints currently gated by DynamicPermission must stay gated; you are
  making the policy *satisfiable*, not optional. No `[AllowAnonymous]` anywhere in this phase.
- Tenancy behavior (404-not-403 on cross-tenant) is correct today — don't touch those handlers except
  for the role-set swap.
- The role-claim seed must be **idempotent** (the seeder runs on every Dev boot) and must not touch
  production posture (respect the refinement-phase-5 config guard patterns).
- Support/Moderation must lose clinical + financial access, but must NOT lose what tickets genuinely
  need (booking summary context for a thread). Check the ticket flows before narrowing a shared query.
- Zero new build warnings; enum/role names are stable wire contracts — don't rename `super_admin` etc.

## 6. Definition of Done

- Integration tests (Testing env, house `WebApplicationFactory` pattern): super_admin 200s on a
  representative endpoint of every admin console family; finance 200s on payouts + 403s on
  verification decide; support 403s on `GET bookings/{id}/care_instructions` and on
  `TransitionBookingStatus`; moderation 200s on the review queue only. Grant/revoke via the new
  controller round-trips.
- `dotnet build Baya.sln` 0 new warnings · `dotnet test Baya.sln` green (all prior tests + new).
- REQ-031 marked delivered; RUNBOOK/report corrections committed.

## 7. How to test (human)

1. Boot per RUNBOOK; log in as 09120000020 (super_admin) via dev OTP.
2. Call `GET api/v1/platform_configs`, the payout batches list, the moderation queue — all 200.
3. Log in as 09120000021 (finance): payouts 200; verification decide → 403.
4. Grant `moderation` to a fresh user via the new RolesController; verify the queue opens for them.

## 8. Hand off & document

- Tick H-04/H-05 in [issues.md](issues.md) with the commit hash.
- Update `for-backend.md` (REQ-031), `server/CLAUDE.md` (authz model paragraph), RUNBOOK.
- Write `dev/shared-working-context/reports/hardening-phase-1-report.md` + a backend handoff note if
  Phase 4 runs as a separate agent.
