文件
hyb-oa/.trellis/tasks/08-10-production-employee-smoke-break-glass-reset/design.md
T
wangsli ec68256588
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
实现(08-10-production-employee-smoke-break-glass-reset): 增加员工Smoke受控重置工具
2026-08-10 21:01:59 +08:00

8.4 KiB
原始文件 Blame 文件历史

生产员工 Smoke Break-glass 重置:设计说明

1. 边界与分阶段授权

本任务修复专用 employee smoke 的认证能力,不改变员工角色或权限,也不执行合同负向验收。整个任务分三层:本地工具实现、正常候选发布、生产账号 reset。task.py start 只进入第一层;发布和账号写入各有独立审批停点。

新脚本必须通过正常发布进入镜像。带外复制虽然改动面看似更小,却绕过 provenance、运行脚本 SHA 和可复现发布合同,不能用于唯一一次生产认证写入。

2. 现有 helper 的真实副作用

repair-oa-only-smoke-accounts.js 的 dry-run 和 apply 共用 planForTarget:

restore_active_employee -> status/employment_status/archived_at
clear_login_lock        -> login_failed_* / login_locked_until
add_missing_roles       -> INSERT employee_roles, never delete extras
every applied target    -> token_version +1 + lifecycle event
password_hash           -> never touched
session/challenge       -> never revoked/invalidated

因此它既不能恢复密码,也不能建立精确角色不变量。本任务仅运行 dry-run 作为“现场仍无需生命周期/角色修复”的独立证明;任何 planned action 都阻塞新脚本 apply,helper apply 永久禁止。

3. 新脚本目标模型

新增 reset-employee-smoke-password.js,复用现有 helper 导出的 smoke 纯判据,但不调用其 execute/applyTarget。目标由数据库状态自行发现,不传 identifier:

status == active && employment_status == active && archived_at == null
&& email/phone identifier ready
&& current login lock is clear
&& must_change_password == false
&& smoke(row.employee_no | row.email | row.name)
&& sorted(roleCodes) == ["employee"]
&& resolvePermissionsForRoles(roleCodes)
     excludes ["*", "project.manage", "project.view_all", "system.settings"]

查询最多保留两个匹配用于判断唯一性,稳定输出只包含 count/boolean。角色精确断言同时保证没有管理员、经理、财务、人事等额外角色;权限断言独立存在,防止未来 ROLE_PERMISSIONS.employee 漂移后角色仍正确但权限已扩大。

4. 状态机

PLANNING
  -> local implementation/tests/read-only review
  -> STOP: deployment approval
  -> commit/push/remoteVerified -> normal deploy -> runtime script SHA match
  -> production read-only identity/migration/window
  -> repair helper dry-run: OK/no actions, apply=0
  -> employee script --preflight: PASS/1/exact role/no manage permissions
  -> old-hash limited material + temp/final secrets ready
  -> STOP: production account-write approval
  -> --apply EMPLOYEE_SMOKE_BREAK_GLASS exactly once
  -> --verify + independent role/permission snapshot
  -> only if post-write negative premise PASS: one login
  -> /change-password once -> stop at /portal
  -> --verify-password-changed + final role/permission snapshot
  -> secret update + sensitive cleanup + read-only reviewer
  -> RESET_COMPLETE -> handoff 08-08 readiness

任一箭头失败即停止。发布失败不会触及账号;apply 结果不确定时仅 --verify,不得重试;写后权限不变量失败时不得登录。

5. 命令接口与稳定输出

脚本接口:

  • --preflight:目标计数、唯一性、smoke/identifier/status/role/permission 布尔。
  • --verify:上述不变量 + must-change、locks、active session/code、audit 状态。
  • --verify-password-changed:上述不变量 + must-change=false、一个新 active session、两类 audit。
  • --diagnose-wecom-binding(可选只读):仅记录绑定/同步/近期认证计数,不输出 external user id。
  • --apply EMPLOYEE_SMOKE_BREAK_GLASS:唯一写入口,密码从 stdin。

错误只输出 allowlist category,例如 employee_smoke_target_not_unique、employee_smoke_target_changed、explicit_confirmation_required、密码输入/策略错误;未知异常只输出安全类别,不回显数据库消息或目标字段。

6. 事务写入合同

apply 在事务外发现唯一目标并取得 token version,生成同源 bcrypt hash;事务内再次读取并核对所有目标/权限不变量。通过后仅执行:

  1. 更新目标 password_hash、must_change_password=true;清三类认证失败/锁定字段与 password-reset request 字段;token version +1。
  2. 以 employee_id=target.id 撤销 active auth sessions、失效 password reset codes、消耗 email login codes。
  3. 写 SYSTEM_EMPLOYEE_SMOKE_BREAK_GLASS_PASSWORD_RESET 审计,actor/target 均为目标,extra 只记录 credentialRecorded=false。

事务不包含 employee_roles、status/employment/archive、identifier、企微或业务表。单测必须对这些缺席路径做断言,而不只验证预期 update 存在。

7. 严格一次与不确定结果

执行前记录专用 attempt 哨兵和本次审计基线;一旦发出 apply,applyExecutionCount 固定为 1。结果分支:

  • 完整 PASS:立即 --verify。
  • 明确 BLOCKED/非零:停止,不重试。
  • SSH 中断、输出缺失、JSON 不可解析:标记 APPLY_UNCERTAIN,只运行 --verify。

verify 若显示 must-change、audit/session/code 等状态已落地,则按成功写入处理并禁止第二次 apply;审计计数相对写前恰好 +1 作为独立交叉证明。

8. 写后权限再确认

写前诊断不是写后证据。apply 后先由脚本 --verify 重算目标、角色和权限,再以独立 BEGIN READ ONLY 查询重算同一组不变量:

targetMatches=1
rolesExact=true
hasWildcard=false
hasProjectManage=false
hasProjectViewAll=false
hasSystemSettings=false

任何一个值不符都停止在无登录状态。不得通过删除角色、改权限或改验收断言补救;这类生产写入需要新任务。

9. 凭据、强制改密与恢复材料

临时/最终密码由受控 CSPRNG 分别生成,均满足应用策略且不相同。临时密码通过 stdin 进入 apply;正式仓库外 employee secret 仅在 verify PASS 后原子更新为正确 identifier + 临时密码,强制改密完成后再替换为最终密码。

旧 hash 只在生产现场 0700/0600 暂存,最晚 apply 后 24 小时销毁。它不是完整或部分回滚;即使另行获批写回,也不能恢复 token、会话、challenge、计数和审计。

浏览器使用单 context、workers=1、retries=0:一次登录必须到 /change-password,一次提交允许到 /portal 后立即关闭;业务 API 发送为 0。随后 --verify-password-changed 和最终独立权限快照都 PASS 才清理材料。

10. 发布与脚本身份

本地实现和只读 reviewer 通过后形成 clean/published 候选。生产发布沿用现有 identity bootstrap、preflight、migration/window 和 deploy-existing.sh,不手工同步单文件。

发布后以 git show <candidate>:server/src/scripts/reset-employee-smoke-password.js 为基线,对运行容器同路径做 SHA-256 比较;只记录 scriptShaMatch=true。脚本不存在或不一致即停止账号阶段。候选没有 migration 时 pending 必须为空;任何意外 migration 或生产门禁 finding 都停止。

11. 失败矩阵

阶段 失败 动作
local tests/review 非 PASS 修复本地代码,不发布
deploy approval/provenance 缺失或不一致 停止,零生产变更
deployment 失败或运行 SHA 不符 停止,零账号写入
helper dry-run planned action / unsafe target PAUSED,helper apply=0
employee preflight 非唯一/角色或权限不符 停止,不选择目标
recovery/secret 无法安全准备 停止,不 apply
apply 明确失败 BLOCKED 停止,不重试
apply 通道不确定 无完整结果 仅 --verify 定性
post-write role/permission 任一不变量失败 禁止登录,另行决策
login/change-password 首次失败 停止,零 retry,不再 apply
cleanup 失败 标记敏感清理未完成,不归档

12. Reviewer 与交接

本地实现后必须启动只读 reviewer,重点检查目标发现、角色/权限精确断言、事务写集、失败类别和测试反例;reviewer 不改代码。生产执行后再由只读 reviewer 核对候选/脚本 SHA、apply 次数、审计增量、目标限定副作用、写后权限证据和材料清理。

最终只向原 readiness task 交接稳定布尔与计数,不交接凭据。该 task 才负责登录后的合同负向验收和最终 readiness 口径。