- 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)
4.5 KiB
4.5 KiB
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.
adminis 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.