完成(08-09-production-admin-break-glass-reset): 恢复管理员认证
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-09 09:18:13 +08:00
父节点 8806521c22
当前提交 4fbfee2ff2
共修改 4 个文件,包含 109 行新增和 40 行删除
@@ -1924,3 +1924,68 @@ node scripts/qa/payment-batch-flag-snapshot.mjs restore --snapshot-file "$REPORT
```tsx ```tsx
<Link to="/mobile/payment-batches">进入付款</Link> <Link to="/mobile/payment-batches">进入付款</Link>
``` ```
## 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
```
@@ -1,6 +1,6 @@
# 生产管理员 break-glass 密码重置:执行计划 # 生产管理员 break-glass 密码重置:执行计划
> 当前状态:IN PROGRESS。用户已于 2026-08-09 完成规划只读复核并明确批准启动;尚未执行生产脚本、账号写入、登录或 secret 修改。 > 当前状态:RESET COMPLETE / REVIEWED。用户已于 2026-08-09 批准并完成本任务认证恢复生命周期;生产写入、强制改密、最终验证与敏感材料清理均已完成,合同业务验收仍留在原 readiness task。
## 1. 规划事实 ## 1. 规划事实
@@ -15,44 +15,44 @@
- [x] 用户完成规划只读复核并明确批准 task start。 - [x] 用户完成规划只读复核并明确批准 task start。
- [x] 用户批准按本任务合同执行生产管理员认证恢复;批准范围包含脚本完整副作用与强制改密的两次密码写入,不包含任何业务页面或合同验收。 - [x] 用户批准按本任务合同执行生产管理员认证恢复;批准范围包含脚本完整副作用与强制改密的两次密码写入,不包含任何业务页面或合同验收。
- [ ] 规划提交已 published,工作区 clean;原 readiness task 保持 `LOGIN PAUSED`。 - [x] 规划提交 `8806521` 已 published,执行前工作区 clean;原 readiness task 保持 `LOGIN PAUSED`。
- [ ] 本地 focused unit test 与 node syntax PASS。 - [x] 本地 focused unit test 6/6 与 node syntax PASS。
- [ ] 生产 identity/read-only preflight、65 migrations、产品 build、合同窗口通过。 - [x] 生产 identity/read-only preflight 为 13/13,65 migrations 全 applied、pending=none,产品 build 匹配候选,窗口为 `OPEN/0`。
- [ ] 运行容器脚本 SHA 与产品候选精确一致。 - [x] 运行容器脚本 SHA 与产品候选精确一致,未复制或替换生产脚本。
## 3. 生产执行清单 ## 3. 生产执行清单
### Phase A:只读前置 ### Phase A:只读前置
- [ ] 运行脚本 `--preflight`,人工核对 PASS/1/true。 - [x] 运行脚本 `--preflight`,结果为 PASS/1/true。
- [ ] 运行写前 `--diagnose-wecom-binding`,记录脱敏布尔/计数。 - [x] 运行写前 `--diagnose-wecom-binding`:目标唯一且已绑定,无绑定冲突;近 30 分钟密码/企微登录与失败计数均为 0。
- [ ] 记录唯一目标的 must-change、token/session/challenge 计数快照;不输出 identifier/hash。 - [x] 写前快照为 must-change=false、locks cleared、active session=1、两类 active challenge=0;该会话过去 24 小时无活动,近 30 分钟无认证活动,按批准副作用撤销。
- [ ] 创建远端 `0700/0600` recovery material,写入旧 hash;验证存在性,stdout 不含内容/路径。 - [x] 创建远端 `0700/0600` recovery material 并验证存在;hash 与实际路径均未回显或进入任务证据。
- [ ] 生成互不相同的临时/最终强密码,写入本机 `0600` 临时 secret;不进入 env/argv/history。 - [x] 生成互不相同且不复用旧 secret 的临时/最终强密码,存入本机 `0600` 临时 secret;未进入 env/argv/history。
### Phase B:唯一 break-glass 写入 ### Phase B:唯一 break-glass 写入
- [ ] 从 stdin 执行一次且仅一次 `--apply ACTIVE_ADMIN_BREAK_GLASS`。 - [x] 写入本地 attempt 哨兵后,从 stdin 执行一次且仅一次 `--apply ACTIVE_ADMIN_BREAK_GLASS`;`applyExecutionCount=1`。
- [ ] 人工读取 JSON:status、target count、password/must-change/token/locks/session/challenge/audit 字段完整且 PASS。 - [x] apply 结构化结果 PASS/1:passwordUpdated、mustChangePassword、tokenVersionIncremented、locksCleared、auditCreated 均为 true;撤销陈旧会话 1,失效两类 challenge 均为 0。
- [ ] 任一通道/结果异常立即停;只允许 `--verify` 定性,禁止第二次 apply。 - [x] apply 通道与结果完整,没有进入 `APPLY_UNCERTAIN`;attempt 哨兵保留至最终清理,执行期间不存在第二次 apply 路径。
- [ ] 运行 `--verify`,要求 must-change、locks、sessions、codes、audit 全部符合脚本合同。 - [x] 紧随 apply 的 `--verify` 七项 PASS:唯一目标、must-change=true、locks cleared、active sessions=0、两类 active codes=0、audit present=true。
### Phase C:secret 与强制改密 ### Phase C:secret 与强制改密
- [ ] `--verify` PASS 后,原子更新仓库外 live-smoke admin identifier/temp password,权限 `0600`。 - [x] `--verify` PASS 后原子更新仓库外 live-smoke admin identifier/temp password,现场复核内容匹配且权限为 `0600`;第一次本地更新未改变文件并被匹配检查挡住,修正后才进入登录,没有消耗登录次数。
- [ ] 通过 stdin 凭据的一次性 Playwright runner 新会话登录一次,确认只跳 `/change-password`。 - [x] 一次性 Playwright runner 通过受控文件描述符读取凭据,新会话登录 1 次并确认跳到 `/change-password`;零 retry。
- [ ] 提交一次强制改密;成功到 `/portal` 后立即停止,不进入业务页面。 - [x] 强制改密提交 1 次;成功到 `/portal` 后立即清理并关闭,非认证业务 API 在客户端被阻断,实际发送数为 0。
- [ ] 运行 `--verify-password-changed`,要求最终 PASS。 - [x] `--verify-password-changed` PASS:must-change=false、locks cleared、active sessions=1,两类审计均存在。
- [ ] 原子更新仓库外 live-smoke admin final password,权限仍为 `0600`。 - [x] 仓库外 live-smoke admin secret 原子更新为最终凭据,内容匹配且权限仍为 `0600`。
- [ ] 运行写后 `--diagnose-wecom-binding`,只记录脱敏布尔/计数。 - [x] 写后 `--diagnose-wecom-binding` PASS:目标与绑定仍唯一、无冲突;本轮 1 次密码登录、0 次失败,未修改企微绑定。
### Phase D:清理与交接 ### Phase D:清理与交接
- [ ] 删除本机临时 secret、Playwright storage/cookie 和远端旧 hash;验证三个 destroyed/cleared 布尔。 - [x] 删除本机临时 secret、Playwright storage/cookie 和远端旧 hash;`temporaryCredentialDestroyed=true`、`browserStateCleared=true`、`recoveryMaterialDestroyed=true`。
- [ ] 复核其他账号/会话未被脚本触及,合同/schema/权限/业务写入为 0。 - [x] 源码与 apply 结果确认 session/challenge 写入均限定唯一目标;浏览器业务 API 发送数 0,合同/schema/权限/其他账号写入为 0。
- [ ] 最终生产只读 status/window 快照。 - [x] 最终生产只读 preflight 为 13/13,migration pending=none,`rollbackWindow=OPEN / terminatedRows=0 / basis=terminated_status_count`。
- [ ] 启动只读 reviewer;按真实结果更新 PRD AC。 - [x] 只读 reviewer 已核对脱敏执行记录、源码与单测;两轮收口 finding 修正后最终结论为 `PASS`,且未宣称 reviewer 独立重跑生产。
- [ ] 向 `08-08-production-authenticated-readiness` 只交接“最终管理员 secret 已就绪”,不交接值;恢复该任务继续合同只读验收。 - [x] 向 `08-08-production-authenticated-readiness` 只交接“最终管理员 secret 已就绪”,不交接值;合同验收继续由该任务完成。
## 4. 允许与禁止的输出 ## 4. 允许与禁止的输出
@@ -73,6 +73,10 @@
- [x] `node scripts/qa/check-sensitive-content.mjs`:PASS,0 findings。 - [x] `node scripts/qa/check-sensitive-content.mjs`:PASS,0 findings。
- [x] `git diff --check`:PASS。 - [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`。
@@ -73,16 +73,16 @@
## Acceptance Criteria ## Acceptance Criteria
- [ ] AC1:执行来源 clean/published,生产身份、65 migration、产品 SHA 和运行脚本 SHA 门禁全部 PASS;未复制或替换生产脚本。 - [x] AC1:执行来源 clean/published,生产身份、65 migration、产品 SHA 和运行脚本 SHA 门禁全部 PASS;未复制或替换生产脚本。
- [ ] AC2:`--preflight` 为 PASS/1/true,写前 `--diagnose-wecom-binding` 与认证状态快照完成;目标不唯一或冲突时没有执行写入。 - [x] AC2:`--preflight` 为 PASS/1/true,写前 `--diagnose-wecom-binding` 与认证状态快照完成;目标唯一且无绑定冲突后才进入写入。
- [ ] AC3:旧 hash 以 `0700/0600` 受控短期材料保存,临时/最终密码由 CSPRNG 独立生成并满足策略;任何敏感值均未回显或进入仓库/task/chat。 - [x] AC3:旧 hash 以 `0700/0600` 受控短期材料保存,临时/最终密码由 CSPRNG 独立生成并满足策略;任何敏感值均未回显或进入仓库/task/chat。
- [ ] AC4:`--apply ACTIVE_ADMIN_BREAK_GLASS` 恰好执行一次且返回结构化 PASS;未执行手写 SQL 写入,未修改角色、启用状态、email 或其他账号。 - [x] AC4:`--apply ACTIVE_ADMIN_BREAK_GLASS` 恰好执行一次且返回结构化 PASS;未执行手写 SQL 写入,未修改角色、启用状态、email 或其他账号。
- [ ] AC5:紧随 apply 的 `--verify` PASS,脚本声明的 must-change、锁、会话、验证码、token/audit 副作用与实际计数自洽。 - [x] AC5:紧随 apply 的 `--verify` PASS,脚本声明的 must-change、锁、会话、验证码、token/audit 副作用与实际计数自洽。
- [ ] AC6:浏览器仅完成一次管理员登录和一次强制改密提交,按预期进入 `/change-password`;未进入合同或其他业务页面,失败时没有自动 retry。 - [x] AC6:浏览器仅完成一次管理员登录和一次强制改密提交,按预期进入 `/change-password`;到 `/portal` 后立即停止,业务 API 实际发送数为 0,失败自动 retry 为 0。
- [ ] AC7:`--verify-password-changed` PASS,最终仓库外 secret 原子更新且 `0600`;临时凭据、浏览器状态和旧 hash 均已销毁。 - [x] AC7:`--verify-password-changed` PASS,最终仓库外 secret 原子更新且 `0600`;临时凭据、浏览器状态和旧 hash 均已销毁。
- [ ] AC8:生产账号写入仅限唯一管理员认证生命周期;其他员工会话/账号、合同、权限、schema 与业务数据写入计数为 0。 - [x] AC8:生产账号写入仅限唯一管理员认证生命周期;其他员工会话/账号、合同、权限、schema 与业务数据写入计数为 0。
- [ ] AC9:只读 reviewer 复核目标唯一性、apply 次数、副作用、secret 清理和证据脱敏;task/process/governance/sensitive/diff 门禁全部 PASS。 - [x] AC9:只读 reviewer 已复核目标唯一性、apply 次数、副作用、secret 清理和证据脱敏;其核对对象为任务脱敏执行记录、源码与单测,不是独立生产重跑。task/process/governance/sensitive/diff 门禁全部 PASS。
- [ ] AC10:任务交接明确说明只恢复管理员认证,不代表合同登录态验收通过;`08-08-production-authenticated-readiness` 继续保持 PAUSED,直至其自身 AC 完成。 - [x] AC10:任务交接明确说明只恢复管理员认证,不代表合同登录态验收通过;`08-08-production-authenticated-readiness` 继续保持 PAUSED,直至其自身 AC 完成。
## Out of Scope ## Out of Scope
@@ -2,7 +2,7 @@
"id": "production-admin-break-glass-reset", "id": "production-admin-break-glass-reset",
"name": "production-admin-break-glass-reset", "name": "production-admin-break-glass-reset",
"title": "生产管理员 break-glass 密码重置", "title": "生产管理员 break-glass 密码重置",
"description": "仅对唯一活跃管理员执行一次既有 break-glass 密码重置脚本;前后使用脚本自带只读门禁,受控保存并销毁旧 hash 有限恢复材料,不包含登录态与合同验收。", "description": "仅对唯一活跃管理员执行一次既有 break-glass 密码重置脚本;前后使用脚本自带只读门禁,受控保存并销毁旧 hash 有限恢复材料,不包含业务登录态与合同验收。",
"status": "in_progress", "status": "in_progress",
"dev_type": null, "dev_type": null,
"scope": "production-unique-admin-break-glass-reset-lifecycle", "scope": "production-unique-admin-break-glass-reset-lifecycle",
@@ -34,6 +34,6 @@
".trellis/spec/hyb-oa/deployment-trialrun.md", ".trellis/spec/hyb-oa/deployment-trialrun.md",
".trellis/spec/hyb-oa/security-privacy.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": {} "meta": {}
} }