- 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)
90 lines
4.5 KiB
Markdown
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.
|