diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index 44b0b66a..a65f7ad4 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -1924,3 +1924,68 @@ node scripts/qa/payment-batch-flag-snapshot.mjs restore --snapshot-file "$REPORT ```tsx 进入付款 ``` + +## Scenario: Unique Production Administrator Break-Glass Reset + +### 1. Scope / Trigger + +- Trigger: the only active production administrator cannot authenticate, self-service reset is unavailable, and a separately approved task restores access with the existing break-glass script. +- Purpose: make the single irreversible reset attempt observable without exposing credentials or treating historical audit rows as proof of the current run. + +### 2. Signatures + +- Read-only commands: `--preflight`, `--verify`, `--verify-password-changed`, `--diagnose-wecom-binding` on `server/src/scripts/reset-active-admin-password.js`. +- Single write command: `node src/scripts/reset-active-admin-password.js --apply ACTIVE_ADMIN_BREAK_GLASS`, with the temporary password on stdin only. +- Browser lifecycle: `/login` -> `/change-password` -> `/portal`, one login submission and one password-change submission. + +### 3. Contracts + +- Compare the running-container script SHA with the reviewed deployed candidate before selecting a target. A workspace copy must never be pushed into the container to make the check pass. +- Create a local apply-attempt sentinel before opening the SSH/container channel. Missing output, non-zero transport status or invalid JSON enters `APPLY_UNCERTAIN`; only `--verify` may classify the result, and apply is never retried. +- Capture the break-glass audit count before apply and require an increment of exactly one afterward. `auditPresent=true` alone is insufficient because a historical reset may already satisfy that boolean. +- `--verify` immediately after apply must prove the unique target, must-change=true, cleared locks, zero active sessions, zero active password-reset codes, zero active email-login codes and a break-glass audit. +- Atomically replace the repository-external smoke secret first with the temporary credential and later with the final credential. Before consuming the one allowed login, read the file back in-process and compare both identifier and password to the controlled source; a successful rename without content agreement is not readiness. +- The browser runner receives credentials through controlled file descriptors/stdin, blocks non-authentication business APIs, submits each form once, clears browser state and does not enter business pages. +- A prior hash may be kept only as approved `0700/0600` short-lived recovery material. Destroy it and all temporary credentials after `--verify-password-changed` passes; evidence stores booleans/counts, never values or paths. + +### 4. Validation & Error Matrix + +- Running script SHA mismatch, target count other than one or binding conflict -> stop before recovery material and apply. +- Active session with recent activity -> stop and confirm operator ownership; an old inactive session may be revoked only when that side effect is already approved. +- Apply channel/result uncertain -> write no second password; run `--verify` only. +- Apply PASS but `--verify` fails -> do not log in; retain controlled materials until a new decision or the custody deadline. +- Secret update content mismatch -> do not consume the login attempt; fix the local atomic update and re-verify without another apply. +- Login or change-password submission fails -> zero retry; preserve the known credential state and request a new decision. +- Recovery-material deletion fails -> task remains incomplete even when the final password works. + +### 5. Good/Base/Bad Cases + +- Good: audit baseline 1 becomes 2 after one apply, immediate verify passes, one login completes forced change, final verify passes, and all temporary material is destroyed. +- Base: audit baseline 0 becomes 1 with the same state-machine checks. +- Bad: accept `auditPresent=true` when the baseline was already positive, blindly retry after an SSH interruption, or attempt login before proving the temporary secret contains the new credential. + +### 6. Tests Required + +- Source review must trace every session/challenge update to `where.employee_id=target.id`. When this script's write behavior changes, focused unit tests must additionally assert those exact `where` arguments; call-count assertions alone do not prove target scoping. +- Unit tests cover non-unique target refusal, transaction-time target change, apply side effects and both verify modes. +- Execution evidence asserts runtime-script SHA match, apply-attempt count 1, audit delta exactly 1, the complete post-apply verify fields, login/change submit counts 1/1 and cleanup booleans. +- Run focused script tests, Node syntax, task/process/governance/sensitive gates and `git diff --check`; an authentication-state-machine task additionally receives a read-only reviewer. + +### 7. Wrong vs Correct + +#### Wrong + +```text +apply output missing -> retry apply +verify.auditPresent=true -> current audit write proved +atomic rename succeeded -> consume the only login attempt +``` + +#### Correct + +```text +create attempt sentinel -> apply once -> uncertain output means verify only +capture audit baseline -> require baseline + 1 +atomic rename -> compare identifier/password in-process -> login once +-> change password once -> final verify -> destroy temporary material +``` diff --git a/.trellis/tasks/08-09-production-admin-break-glass-reset/implement.md b/.trellis/tasks/08-09-production-admin-break-glass-reset/implement.md index 1abc8ba0..02a8c728 100644 --- a/.trellis/tasks/08-09-production-admin-break-glass-reset/implement.md +++ b/.trellis/tasks/08-09-production-admin-break-glass-reset/implement.md @@ -1,6 +1,6 @@ # 生产管理员 break-glass 密码重置:执行计划 -> 当前状态:IN PROGRESS。用户已于 2026-08-09 完成规划只读复核并明确批准启动;尚未执行生产脚本、账号写入、登录或 secret 修改。 +> 当前状态:RESET COMPLETE / REVIEWED。用户已于 2026-08-09 批准并完成本任务认证恢复生命周期;生产写入、强制改密、最终验证与敏感材料清理均已完成,合同业务验收仍留在原 readiness task。 ## 1. 规划事实 @@ -15,44 +15,44 @@ - [x] 用户完成规划只读复核并明确批准 task start。 - [x] 用户批准按本任务合同执行生产管理员认证恢复;批准范围包含脚本完整副作用与强制改密的两次密码写入,不包含任何业务页面或合同验收。 -- [ ] 规划提交已 published,工作区 clean;原 readiness task 保持 `LOGIN PAUSED`。 -- [ ] 本地 focused unit test 与 node syntax PASS。 -- [ ] 生产 identity/read-only preflight、65 migrations、产品 build、合同窗口通过。 -- [ ] 运行容器脚本 SHA 与产品候选精确一致。 +- [x] 规划提交 `8806521` 已 published,执行前工作区 clean;原 readiness task 保持 `LOGIN PAUSED`。 +- [x] 本地 focused unit test 6/6 与 node syntax PASS。 +- [x] 生产 identity/read-only preflight 为 13/13,65 migrations 全 applied、pending=none,产品 build 匹配候选,窗口为 `OPEN/0`。 +- [x] 运行容器脚本 SHA 与产品候选精确一致,未复制或替换生产脚本。 ## 3. 生产执行清单 ### Phase A:只读前置 -- [ ] 运行脚本 `--preflight`,人工核对 PASS/1/true。 -- [ ] 运行写前 `--diagnose-wecom-binding`,记录脱敏布尔/计数。 -- [ ] 记录唯一目标的 must-change、token/session/challenge 计数快照;不输出 identifier/hash。 -- [ ] 创建远端 `0700/0600` recovery material,写入旧 hash;验证存在性,stdout 不含内容/路径。 -- [ ] 生成互不相同的临时/最终强密码,写入本机 `0600` 临时 secret;不进入 env/argv/history。 +- [x] 运行脚本 `--preflight`,结果为 PASS/1/true。 +- [x] 运行写前 `--diagnose-wecom-binding`:目标唯一且已绑定,无绑定冲突;近 30 分钟密码/企微登录与失败计数均为 0。 +- [x] 写前快照为 must-change=false、locks cleared、active session=1、两类 active challenge=0;该会话过去 24 小时无活动,近 30 分钟无认证活动,按批准副作用撤销。 +- [x] 创建远端 `0700/0600` recovery material 并验证存在;hash 与实际路径均未回显或进入任务证据。 +- [x] 生成互不相同且不复用旧 secret 的临时/最终强密码,存入本机 `0600` 临时 secret;未进入 env/argv/history。 ### Phase B:唯一 break-glass 写入 -- [ ] 从 stdin 执行一次且仅一次 `--apply ACTIVE_ADMIN_BREAK_GLASS`。 -- [ ] 人工读取 JSON:status、target count、password/must-change/token/locks/session/challenge/audit 字段完整且 PASS。 -- [ ] 任一通道/结果异常立即停;只允许 `--verify` 定性,禁止第二次 apply。 -- [ ] 运行 `--verify`,要求 must-change、locks、sessions、codes、audit 全部符合脚本合同。 +- [x] 写入本地 attempt 哨兵后,从 stdin 执行一次且仅一次 `--apply ACTIVE_ADMIN_BREAK_GLASS`;`applyExecutionCount=1`。 +- [x] apply 结构化结果 PASS/1:passwordUpdated、mustChangePassword、tokenVersionIncremented、locksCleared、auditCreated 均为 true;撤销陈旧会话 1,失效两类 challenge 均为 0。 +- [x] apply 通道与结果完整,没有进入 `APPLY_UNCERTAIN`;attempt 哨兵保留至最终清理,执行期间不存在第二次 apply 路径。 +- [x] 紧随 apply 的 `--verify` 七项 PASS:唯一目标、must-change=true、locks cleared、active sessions=0、两类 active codes=0、audit present=true。 ### Phase C:secret 与强制改密 -- [ ] `--verify` PASS 后,原子更新仓库外 live-smoke admin identifier/temp password,权限 `0600`。 -- [ ] 通过 stdin 凭据的一次性 Playwright runner 新会话登录一次,确认只跳 `/change-password`。 -- [ ] 提交一次强制改密;成功到 `/portal` 后立即停止,不进入业务页面。 -- [ ] 运行 `--verify-password-changed`,要求最终 PASS。 -- [ ] 原子更新仓库外 live-smoke admin final password,权限仍为 `0600`。 -- [ ] 运行写后 `--diagnose-wecom-binding`,只记录脱敏布尔/计数。 +- [x] `--verify` PASS 后原子更新仓库外 live-smoke admin identifier/temp password,现场复核内容匹配且权限为 `0600`;第一次本地更新未改变文件并被匹配检查挡住,修正后才进入登录,没有消耗登录次数。 +- [x] 一次性 Playwright runner 通过受控文件描述符读取凭据,新会话登录 1 次并确认跳到 `/change-password`;零 retry。 +- [x] 强制改密提交 1 次;成功到 `/portal` 后立即清理并关闭,非认证业务 API 在客户端被阻断,实际发送数为 0。 +- [x] `--verify-password-changed` PASS:must-change=false、locks cleared、active sessions=1,两类审计均存在。 +- [x] 仓库外 live-smoke admin secret 原子更新为最终凭据,内容匹配且权限仍为 `0600`。 +- [x] 写后 `--diagnose-wecom-binding` PASS:目标与绑定仍唯一、无冲突;本轮 1 次密码登录、0 次失败,未修改企微绑定。 ### Phase D:清理与交接 -- [ ] 删除本机临时 secret、Playwright storage/cookie 和远端旧 hash;验证三个 destroyed/cleared 布尔。 -- [ ] 复核其他账号/会话未被脚本触及,合同/schema/权限/业务写入为 0。 -- [ ] 最终生产只读 status/window 快照。 -- [ ] 启动只读 reviewer;按真实结果更新 PRD AC。 -- [ ] 向 `08-08-production-authenticated-readiness` 只交接“最终管理员 secret 已就绪”,不交接值;恢复该任务继续合同只读验收。 +- [x] 删除本机临时 secret、Playwright storage/cookie 和远端旧 hash;`temporaryCredentialDestroyed=true`、`browserStateCleared=true`、`recoveryMaterialDestroyed=true`。 +- [x] 源码与 apply 结果确认 session/challenge 写入均限定唯一目标;浏览器业务 API 发送数 0,合同/schema/权限/其他账号写入为 0。 +- [x] 最终生产只读 preflight 为 13/13,migration pending=none,`rollbackWindow=OPEN / terminatedRows=0 / basis=terminated_status_count`。 +- [x] 只读 reviewer 已核对脱敏执行记录、源码与单测;两轮收口 finding 修正后最终结论为 `PASS`,且未宣称 reviewer 独立重跑生产。 +- [x] 向 `08-08-production-authenticated-readiness` 只交接“最终管理员 secret 已就绪”,不交接值;合同验收继续由该任务完成。 ## 4. 允许与禁止的输出 @@ -73,6 +73,10 @@ - [x] `node scripts/qa/check-sensitive-content.mjs`:PASS,0 findings。 - [x] `git diff --check`:PASS。 -## 6. 当前结论 +## 6. 执行记录与当前结论 -规划已获用户只读复核与启动批准。下一步先提交并发布规划,使执行来源满足 clean/published;随后进入生产只读门禁。所有前置门禁通过前,不生成密码、不保存旧 hash、不执行账号写入。 +- 写前辅助 SHA 探测首次误用 `docker compose`,在进入容器前被远端 legacy Compose 能力拒绝;随后按部署脚本的实际选择改用 `docker-compose`。该失败为只读调用错误,账号/数据库写入为 0。 +- 写前 break-glass 审计基线为 1;写后为 2,增量恰好 1。token version 相对写前基线上升,且 apply 返回自身 token increment 断言为 true。 +- 唯一 apply、唯一登录、唯一强制改密均已完成;最终 secret 可用性由应用登录和 `--verify-password-changed` 双重证明。 +- 敏感恢复材料与一次性凭据均已销毁,不再保留 24 小时恢复窗口。该销毁不影响已经写入的审计与最终密码。 +- 当前只完成管理员认证恢复,不代表合同登录态验收通过。只读 reviewer 已完成;下一步恢复 `08-08-production-authenticated-readiness`。 diff --git a/.trellis/tasks/08-09-production-admin-break-glass-reset/prd.md b/.trellis/tasks/08-09-production-admin-break-glass-reset/prd.md index 5e81c07f..c2e960d0 100644 --- a/.trellis/tasks/08-09-production-admin-break-glass-reset/prd.md +++ b/.trellis/tasks/08-09-production-admin-break-glass-reset/prd.md @@ -73,16 +73,16 @@ ## Acceptance Criteria -- [ ] AC1:执行来源 clean/published,生产身份、65 migration、产品 SHA 和运行脚本 SHA 门禁全部 PASS;未复制或替换生产脚本。 -- [ ] AC2:`--preflight` 为 PASS/1/true,写前 `--diagnose-wecom-binding` 与认证状态快照完成;目标不唯一或冲突时没有执行写入。 -- [ ] AC3:旧 hash 以 `0700/0600` 受控短期材料保存,临时/最终密码由 CSPRNG 独立生成并满足策略;任何敏感值均未回显或进入仓库/task/chat。 -- [ ] AC4:`--apply ACTIVE_ADMIN_BREAK_GLASS` 恰好执行一次且返回结构化 PASS;未执行手写 SQL 写入,未修改角色、启用状态、email 或其他账号。 -- [ ] AC5:紧随 apply 的 `--verify` PASS,脚本声明的 must-change、锁、会话、验证码、token/audit 副作用与实际计数自洽。 -- [ ] AC6:浏览器仅完成一次管理员登录和一次强制改密提交,按预期进入 `/change-password`;未进入合同或其他业务页面,失败时没有自动 retry。 -- [ ] AC7:`--verify-password-changed` PASS,最终仓库外 secret 原子更新且 `0600`;临时凭据、浏览器状态和旧 hash 均已销毁。 -- [ ] AC8:生产账号写入仅限唯一管理员认证生命周期;其他员工会话/账号、合同、权限、schema 与业务数据写入计数为 0。 -- [ ] AC9:只读 reviewer 复核目标唯一性、apply 次数、副作用、secret 清理和证据脱敏;task/process/governance/sensitive/diff 门禁全部 PASS。 -- [ ] AC10:任务交接明确说明只恢复管理员认证,不代表合同登录态验收通过;`08-08-production-authenticated-readiness` 继续保持 PAUSED,直至其自身 AC 完成。 +- [x] AC1:执行来源 clean/published,生产身份、65 migration、产品 SHA 和运行脚本 SHA 门禁全部 PASS;未复制或替换生产脚本。 +- [x] AC2:`--preflight` 为 PASS/1/true,写前 `--diagnose-wecom-binding` 与认证状态快照完成;目标唯一且无绑定冲突后才进入写入。 +- [x] AC3:旧 hash 以 `0700/0600` 受控短期材料保存,临时/最终密码由 CSPRNG 独立生成并满足策略;任何敏感值均未回显或进入仓库/task/chat。 +- [x] AC4:`--apply ACTIVE_ADMIN_BREAK_GLASS` 恰好执行一次且返回结构化 PASS;未执行手写 SQL 写入,未修改角色、启用状态、email 或其他账号。 +- [x] AC5:紧随 apply 的 `--verify` PASS,脚本声明的 must-change、锁、会话、验证码、token/audit 副作用与实际计数自洽。 +- [x] AC6:浏览器仅完成一次管理员登录和一次强制改密提交,按预期进入 `/change-password`;到 `/portal` 后立即停止,业务 API 实际发送数为 0,失败自动 retry 为 0。 +- [x] AC7:`--verify-password-changed` PASS,最终仓库外 secret 原子更新且 `0600`;临时凭据、浏览器状态和旧 hash 均已销毁。 +- [x] AC8:生产账号写入仅限唯一管理员认证生命周期;其他员工会话/账号、合同、权限、schema 与业务数据写入计数为 0。 +- [x] AC9:只读 reviewer 已复核目标唯一性、apply 次数、副作用、secret 清理和证据脱敏;其核对对象为任务脱敏执行记录、源码与单测,不是独立生产重跑。task/process/governance/sensitive/diff 门禁全部 PASS。 +- [x] AC10:任务交接明确说明只恢复管理员认证,不代表合同登录态验收通过;`08-08-production-authenticated-readiness` 继续保持 PAUSED,直至其自身 AC 完成。 ## Out of Scope diff --git a/.trellis/tasks/08-09-production-admin-break-glass-reset/task.json b/.trellis/tasks/08-09-production-admin-break-glass-reset/task.json index 9f4f9015..171f953e 100644 --- a/.trellis/tasks/08-09-production-admin-break-glass-reset/task.json +++ b/.trellis/tasks/08-09-production-admin-break-glass-reset/task.json @@ -2,7 +2,7 @@ "id": "production-admin-break-glass-reset", "name": "production-admin-break-glass-reset", "title": "生产管理员 break-glass 密码重置", - "description": "仅对唯一活跃管理员执行一次既有 break-glass 密码重置脚本;前后使用脚本自带只读门禁,受控保存并销毁旧 hash 有限恢复材料,不包含登录态与合同验收。", + "description": "仅对唯一活跃管理员执行一次既有 break-glass 密码重置脚本;前后使用脚本自带只读门禁,受控保存并销毁旧 hash 有限恢复材料,不包含业务登录态与合同验收。", "status": "in_progress", "dev_type": null, "scope": "production-unique-admin-break-glass-reset-lifecycle", @@ -34,6 +34,6 @@ ".trellis/spec/hyb-oa/deployment-trialrun.md", ".trellis/spec/hyb-oa/security-privacy.md" ], - "notes": "仅规划,尚未启动实施。任务只负责唯一活跃管理员 break-glass 重置生命周期:只读 preflight/企微诊断、单次脚本 apply、只读 verify、一次登录完成强制改密、verify-password-changed、受控 secret 更新与旧 hash 销毁;不进入合同或其他业务页面。完成后回到 08-08-production-authenticated-readiness 继续只读验收。", + "notes": "认证恢复生命周期已完成:只读 preflight/企微诊断、单次脚本 apply、只读 verify、一次登录完成强制改密、verify-password-changed、最终 secret 更新与旧 hash 销毁均通过;只读 reviewer 核对脱敏执行记录后通过。任务未进入合同或其他业务页面,后续回到 08-08-production-authenticated-readiness 继续合同只读验收。", "meta": {} -} \ No newline at end of file +}