---
type: plan
milestone: M3 — the full BW Auth feature suite
---

# M3 — exercise the full BW Auth standard in Scout

Purpose is double: give rian the complete account-management surface, and
**dogfood the BW Auth kit hard enough to find its edges**. Every tension, gap,
or bug found goes in `notes/bw-auth-feedback.md`, which is the deliverable back
to the auth side. Finding a rough edge here is success, not failure.

## What rian asked for, mapped

| Ask | Mechanism | Status |
|---|---|---|
| View As any user (super admin) | `bw_view_as.py` drop-in | build |
| Create levels + attach permissions | kit `create_level`/`set_level_def` + Scout UI | build |
| Per-project and app-wide assignment | M2 | done |
| A "manager" level (projects + people + invites) | new perms + endpoints, then rian creates the level in the UI | build |
| A scoped level (invite + manage members of one project) | per-project `scout.project.members` | build |
| All users × projects at a glance | access-matrix endpoint + screen | build |
| Change password / reset from login screen | central `/account` link + `send_reset` + anonymous forgot | build |
| Review whatever else the kit can do | Auth tab: status, sync, cross-app access lookup | build |

## Locked decisions (D25–D32)

**D25 — View As policy: the owner, plus any level holding `scout.view_as`.**
Owner-only would satisfy the ask, but a permission string lets rian *test*
delegating it from the level editor without a code change — which is the point
of this milestone. Read-only by default; `mode='act'` is offered only to the
owner (acting as a client writes reviews under the client's name — useful for
testing, dangerous for anyone else). rank_of: owner 1000 / admin 3 / lead 2 /
reviewer 1, so a non-owner impersonator can never view a peer or better.

**D26 — verify() runs in the identity resolvers; the write-block lives in the
middleware.** `optional_account`/`current_account` re-authorize any active
view-as fail-closed and then resolve the EFFECTIVE user, so every capability
and visibility answer below them is automatically the target's. The middleware
refuses mutating methods while impersonating read-only (exempt: `/api/view-as/
stop`, `/logout`), so no individual route needs to remember.

**D27 — Capabilities in `/api/me` follow the effective user; the view-as block
carries the real one.** That is what makes the rendered app genuinely "what
darren sees" while the banner still knows who you are. `can_view_as_others` is
computed on the REAL user (the picker is rendered outside the impersonated
view, stop-first).

**D28 — Assignable levels come from the actor, not a constant.** Levels are
rows; a hardcoded "project levels" list goes stale the moment rian creates one.
`Meta.project_levels` is removed; `Me.assignable_levels` (computed by the kit
per actor) drives every level `<select>`. The kit's assignability guard stays
the enforcement; the UI just stops offering what would be refused.

**D29 — Permissions have a catalog with descriptions, and unknown strings are
allowed.** The kit accepts arbitrary permission strings but ships no metadata,
so Scout maintains the catalog (standard 11 + `scout.*`) with labels and an
`enforced` flag. The level editor shows unknown strings as "custom — nothing in
Scout checks this yet", which is the honest truth and itself a feedback item.

**D30 — Membership management becomes its own permission.** New per-project
`scout.project.members`; member add/level/remove routes accept `scout.project.
manage` OR `scout.project.members`. This is what makes rian's second example
level expressible: invite + manage members of exactly one project, nothing else.
Invites are gated app-wide (`accounts.add`) OR by holding members/manage on the
target project.

**D31 — Project deletion exists now, per-project.** `scout.projects.delete`,
checked via `project_can` like everything else; admin gains it by migration
0005. Deletion is refused while the project has reviews unless `?force=true`
(the UI asks twice), removes the project's files from disk, and republishes the
catalog.

**D32 — Anonymous "forgot password" is a Scout stopgap, flagged as such.**
`POST /auth/forgot` (public, throttled, constant response). The right home is
the central login page — recorded as a headline feedback item, built anyway
because testing the boundary is the assignment. `send_reset` is server-scoped
to users Scout has reported, and never discloses whether mail was sent.

## API deltas (the UI contract)

- `Me` gains: `impersonating`, `viewing_as`, `viewing_as_label`, `real_user`,
  `view_as_mode`, `can_write` (false only while impersonating read-only),
  `can_view_as_others`, `assignable_levels: string[]`, `account_url` (central
  manage-account link), `can_delete_projects` per-... (no: deletion is
  per-project via `ProjectOut.can_delete`).
- `ProjectOut` gains `can_delete`, `can_manage_members`.
- `Meta` loses `project_levels` (D28).
- New: `GET /api/view-as/targets`, `POST /api/view-as/start {target, mode?}`,
  `POST /api/view-as/stop`.
- New: `GET /api/levels` (defs + usage + permission catalog + `assignable_by_me`),
  `POST /api/levels`, `PATCH /api/levels/{name}`, `DELETE /api/levels/{name}` —
  kit-guarded; `AccountsError.code` surfaces verbatim.
- New: `POST /api/accounts/invite` (BW invite email → provision → optional
  project grant), `POST /api/accounts/{username}/send-reset`,
  `POST /api/me/send-reset`, `GET /api/accounts/{username}/bw-access`.
- New: `GET /api/access-matrix`, `GET /api/auth/status`, `POST /api/auth/sync`.
- New public: `POST /auth/forgot {username}` (generic response, throttled).
- `DELETE /api/projects/{id}?force=` per D31.

## Verification bar

pytest over every new guard (view-as authorization/rank/auto-stop/write-block,
level CRUD permissions, invite gating incl. per-project, delete-project rules,
forgot-password constant response) with all outbound BW calls stubbed; live
checks for view-as round-trip and gate shapes with minted sessions. No live
invite/reset emails fired at verification time — those are rian's manual pass,
by design (they send real mail).

## Feedback capture

`notes/bw-auth-feedback.md` — running, structured (works / tension / gap / bug),
written as the milestone proceeds, not reconstructed after.
