NextMyDay/docs/specs/spec-007-roles-chain-recovery.md
Claus Lohmar 57610cdbae docs: add the project plan, M0/M0.1 specs, agent guide and README
- PROJECT_PLAN.md: the decision log of record through D-042 and the M0.1 slice
- docs/specs: SPEC-002…009 — routing · door · identity/sessions · /setup ·
  enroll/login · roles/chain/recovery · module registry · the web layer & PWA
  (plus docs/specs/README.md, the index)
- AGENT.md: the working rules — commit AND push at every submilestone (gate
  green), Conventional Commits, never commit secrets
- README.md: the quickstart, the first-run/SSH-tunnel steps and the
  localhost/loopback caveat (SPEC-009 §13 Q-3): with an https base URL the
  CSRF/session cookies are Secure, so reach the app at localhost/127.0.0.1
- config.example.yaml: the settings template (D-035)
2026-10-14 10:53:44 +00:00

90 lines
4.5 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). |
**Follow-up (deferred):** notify the **wider chain**, not only the direct manager.