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

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