# 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.