NextMyDay/docs/specs/spec-007-roles-chain-recovery.md
Claus Lohmar 04c2394c34 docs: record the M0 review fixes and correct the README
REVIEW-001 triage updated (H1, H2, M1c, M1s, L3s, L1c/L2s, L8c, M2s, M3s,
M2c, L1s, L5s, L4s, L7c marked FIXED; L3c, L4c, L5c, L6c stay open with the
reason), and each fix is tied to its code and test.

- SPEC-003: the door pipeline (XFF right→left, the per-request setup gate,
  the isolated hardening headers, the pinned setup module), §6 the `__Host-`
  cookie and the startup-generated signing keys, §7 loopback enforcement
  per request, new AC-10…AC-12, D-2 amended (session_key removed), and a new
  §11 that maps every REVIEW-001 finding to its fix.
- SPEC-005 / SPEC-006 / SPEC-007: the same three points where they belong
  (the wizard's loopback + module pin, the nudge's configured-origin signal,
  the async /access notification ruled in).
- PROJECT_PLAN §10 register: A13 re-stated as enforced (per request + pin);
  A2/A5/A8 re-stated as enforced where the review verified them; A3/A6/A9/A15
  now enforced by this pass; A11 left open (L4c). §14 change log entries.
- README: the status said "planning / pre-alpha · nothing is built" — it is
  stale. State M0+M0.1, note that /setup PERSISTS config.yaml and a restart
  adopts the generated secrets, and state the per-request loopback rule.
2026-10-14 11:30:17 +00:00

91 lines
4.9 KiB
Markdown

# SPEC-007 — roles, the chain & recovery
| Field | Value |
|---|---|
| **Status** | **Implemented** (2026-10-13) — see §7; `build`/`vet`/`test`/`-race` green · static binary · live-smoked |
| **Created** | 2026-10-13 |
| **Author** | OpenArchitect |
| **Target milestone** | **M0** |
| **Governing decisions** | **D-007** · **D-009** · **D-019** · **D-020** · **D-033** · **D-034** · `PROJECT_PLAN.md` §6.4/§7 |
| **Related** | SPEC-004 (identity) · SPEC-006 (login) |
---
## 1. Goal
The **role ladder**, the **manager chain**, and the two **recovery** paths.
Onboarding is **top-down** (D-019): a manager always exists before their reports.
## 2. Roles (D-034) — simple tiers, no ACL, one per user
```
admin > director > manager > team
```
- The assigned role **ripples down** — a director may do all a manager/team member can.
- **One role per user.** The **chain** (`manager_user_id`) is separate: it answers *who approves
you*, not *what you may do*.
## 3. The chain & creation authority (D-019)
- **Creation authority = a role strictly above the new user's**; the new user's manager defaults to
the creator. `admin` is the root (`manager_user_id = NULL`).
- **Top-down:** the admin creates the first directors; directors create managers; managers create team.
## 4. Recovery — non-admin (manager escalation)
```
GET /access → enter the email on record
POST /access?do=auth&action=recover → ALWAYS generic ("if a record exists, we've emailed you")
└─ match → recovery_request(pending) · notify the approver chain + the employee
Approver (**an ancestor in the chain** — the manager upward — or the **admin**) → approve / deny (approver ≠ requester · both audited)
└─ on approval: revoke the OLD identity → mint a NEW GUID → email the enrolment link
Employee re-enrols Passkey + TOTP under the NEW GUID → /{old GUID} permanently dead
```
- Pending requests **expire** (~72h); a capped number per user.
- **Email only initiates**; a **human in the chain authorises** (D-009). Identity rotation (D-007)
means a compromised inbox cannot take over alone.
## 5. Recovery — the admin (config-gated)
```
operator sets admin_recovery: true in config.yaml → restart
→ rotate user 1's identity (pending_enrollment)
→ EMAIL the enrolment link to user 1's ADDRESS ON RECORD
→ audited · the flag is cleared
```
Without the flag, **nothing happens** (dual control, D-033).
## 6. Acceptance criteria
| # | Criterion |
|---|---|
| AC-1 | Creating a user requires a role **above** theirs; the creator is the default manager |
| AC-2 | A user cannot approve their **own** recovery request |
| AC-3 | Approval revokes the old identity + all its sessions and mints a new GUID |
| AC-4 | `/access` never reveals whether the email matched (generic response) |
| AC-5 | `admin_recovery: false` ⇒ no rotation, no email |
| AC-6 | Every request, approval, denial, rotation and enable is audited (A11) |
---
## 7. Implementation notes (accepted rulings)
Built 2026-10-13. `build` · `vet` · `test` · `-race` green; static binary; live-smoked —
`/access` returns a **byte-identical generic page** for known and unknown email; admin flag **on** →
rotate + email + flag cleared; flag **off** → nothing. **Step 0** landed (R-6 secrets generated ·
F-2 `no-store`).
| # | Builder choice | Ruling |
|---|---|---|
| D-1 | Approve/deny at `POST /{GUID}?do=recovery&action=approve\|deny&request={id}` (session-gated) | **Accepted** — the request action lives under the approver's own GUID. |
| **R-7** | Approver rule = *"a role strictly above the requester, never self"*. | **Change** — the authority is the **chain**: the requester's **manager upward** (an ancestor) **or the admin**. §4 amended. **Landed with SPEC-008 (Step 0):** `users.CanApprove(approver, requester, approverIsAncestor)` + `recovery.isAncestor`; `TestCanApproveChainRule` and `TestSpec007AC2_R7_ChainAuthority`. |
| D-2 | Only the immediate manager + the employee are notified | **Accepted** for M0; wider chain fan-out deferred. |
| D-3 | Claim-then-rotate; on a rotation failure the request stays *approved + audited* (never double-rotates) | **Accepted** — safe. |
| D-4 | A live pending request is **reused**, so the cap (3) is a hard bound only under concurrency | **Accepted.** |
| D-5 | Admin-recovery flag clearing rewrites `config.yaml`; unwritable → logged, continue | **Accepted** (D-035). |
| D-6 | The `/access` notification runs **off the request path** (a `Dispatch` seam; `go fn()` by default, context detached with `context.WithoutCancel`, bounded by a 30s timeout) | **Accepted 2026-10-14** (REVIEW-001 **M2s**): a hit must not be timed against a miss — a synchronous send was an enumeration oracle (A3). `Deps.Dispatch` lets tests run it synchronously for determinism. |
**Follow-up (deferred):** notify the **wider chain**, not only the direct manager.