14 KiB
14 KiB
生产员工 Smoke Break-glass 重置
Goal
为 08-08-production-authenticated-readiness 已只读收敛出的唯一专用 OA-only smoke employee 恢复可控登录凭据,同时保持其角色精确为 employee、不获得合同管理权限、不影响真人员工。任务只负责重置生命周期;员工合同权限负向验收仍回到原 readiness task 完成。
本任务含本地安全工具实现、受控发布和一次生产账号写入三个风险层。任务启动只授权本地实现;发布候选与生产账号写入必须分别取得明确批准,任何一步均不得由前一步批准推定。
Background and Confirmed Facts
08-08-production-authenticated-readiness已在生产以只读方式从 5 个活跃非管理员候选中收敛出唯一合格目标:identifier ready、未锁定、失败计数为 0、无需首次改密、可进入合同只读范围且不具备合同管理权限;该目标符合运行容器既有 smoke 命名判据,不是真人员工。员工登录 attempts 保持 0。- 原仓库外员工 smoke 密码对 5 个候选均不匹配,必须恢复凭据后才能完成员工登录态负向验收。
repair-oa-only-smoke-accounts.js:357-367,391-446的 repair 语义不是密码重置:它可恢复 active/employment/archive 状态、清登录锁、递增 token version、补缺失角色并写 lifecycle event,但不更新password_hash。- 该 helper 的角色写入使用
INSERT ... ON CONFLICT DO NOTHING,只补角色、不删除额外角色,因此不能证明或恢复“角色恰好等于期望列表”;其报告还明确声明passwordReset=false(:520-523)。 - helper 的默认 dry-run 使用
BEGIN READ ONLY(:540-588),且isSafeOaOnlySmokeTarget(:110-130)可作为目标安全判据;--apply同时要求 CLI flag 与OA_ONLY_SMOKE_REPAIR_APPLY=1(:37-41,259-263)。 - 管理员既有
reset-active-admin-password.js已证明同源 bcrypt、stdin 密码、字面确认串、事务内二次核对、目标限定的 session/challenge 撤销、审计和APPLY_UNCERTAIN -> verify模式可行;但不得把其目标面扩展到任意员工。 - 新员工脚本当前不在生产镜像。禁止通过 SSH 临时复制、
docker cp、工作区覆盖、node -e或手写 SQL 注入;必须先作为正常候选发布,再以运行镜像脚本 SHA 与该候选精确一致为执行前提。 - 生产合同终止回滚窗口最近九次均为
OPEN / terminatedRows=0。本任务不读写合同;登录和强制改密会产生预期认证写入,但不得进入业务页面。
Scope
- 新增员工 smoke 专用 break-glass 密码重置脚本及 focused tests;不改管理员脚本的目标语义,不改现有 repair helper 的 apply 行为。
- 在本地证明脚本只选择唯一安全目标,角色必须精确为
['employee'],权限不得包含project.manage、project.view_all、system.settings或*,并在事务内复核这些不变量。 - 本地实现通过只读 reviewer 后,另行获批提交、推送并走现有生产发布合同,使脚本进入运行镜像;不得带外复制脚本。
- 另行获批后执行一次生产 reset:repair helper 仅 dry-run 对照,新脚本 preflight、单次 apply、verify、写后角色/权限复核、一次登录与一次强制改密、最终 verify 和 secret/恢复材料清理。
- 完成后只向
08-08-production-authenticated-readiness交接“员工最终凭据已就绪且负向权限前提仍成立”,不在本任务执行合同 UI/API 负向验收。
Requirements
R1 现有 repair helper 只读对照
- 获批进入生产窗口后,先对唯一目标运行一次默认
--dry-run,保存脱敏动作集合与passwordReset=false结论;不得运行其--apply。 - dry-run 若计划
restore_active_employee、clear_login_lock或add_missing_roles,说明现场已偏离既有诊断,本任务立即PAUSED,不得让 helper 修复后继续。 - dry-run 必须继续通过
isSafeOaOnlySmokeTarget,且不能用其“包含期望角色”结论替代精确角色断言。
R2 新脚本的唯一目标与权限不变量
- 新增
server/src/scripts/reset-employee-smoke-password.js。脚本自行从生产状态发现目标,不从 argv/env 接收完整 identifier,不输出 email、phone、employee_no、name 或目标 id。 - 目标必须同时满足:
status=active、employment_status=active、未 archived、smoke 判据命中、至少一个合法 email/phone 登录 identifier、当前未锁定、must_change_password=false、实际角色排序后精确等于['employee']。 - 从实际角色解析出的权限不得包含
project.manage、project.view_all、system.settings或*。候选数不是恰好 1、角色多/少一项、权限扩大、状态变化或 smoke 判据失败均BLOCKED。 - 脚本只断言角色和权限,禁止 create/delete/update
employee_roles,禁止激活、归档、修改 employment/status、identifier 或任何业务数据。 - 事务内必须按目标 id、写前 token version、状态、smoke、精确角色和权限再次核对;任一变化导致事务零写入。
R3 受控密码重置合同
- 提供
--preflight、--verify、--verify-password-changed;可提供只读--diagnose-wecom-binding。唯一写入口必须是--apply EMPLOYEE_SMOKE_BREAK_GLASS,确认串不精确即explicit_confirmation_required。 - 临时密码只从 stdin 读取,使用应用同源
hashPassword与validatePassword;不得进入 argv、环境变量、history、stdout、artifact、task 或对话。 - 单次事务仅更新目标认证状态:新 password hash、
must_change_password=true、三类失败/锁定域与 password-reset request 状态清零、token version +1;仅撤销目标 session、失效目标 password reset code、消耗目标 email login code,并写专用 break-glass audit。 - 所有 session/challenge 写入必须带
employee_id=target.id;审计 actor/target 仅为该目标。不得修改角色、权限、启用/归档状态、企微绑定或其他账号。 - apply 必须严格一次。通道中断、输出缺失或 JSON 不可解析进入
APPLY_UNCERTAIN,此时只允许--verify;verify 证明已落地时禁止 retry。
R4 写后重新验证负向前提
--verify不只验证 must-change、锁、session/challenge 与 audit,还必须重新发现唯一目标并证明角色精确为['employee']、四项合同管理权限均为 false。- verify 后再执行一份独立只读角色/权限快照,与写前基线逐项对账;不能沿用
08-08的旧诊断,也不能只断言“包含 employee”。 - 只有
rolesExact=true、contractManagePermissions=false、目标仍唯一且 reset 副作用全部 PASS,才允许更新临时仓库外 secret 并进行一次登录。
R5 强制改密、secret 与交接
- 写前生成互不相同的临时密码和最终密码;保存旧 hash 为生产现场
0700/0600、最多 24 小时的高敏有限恢复材料。它不能恢复 token version、session、challenge、锁定计数或审计,不得称为回滚。 - 一次登录必须按预期进入
/change-password;一次强制改密提交成功后立即停在/portal,不得主动进入合同或其他业务页、不得发送非认证业务 mutation,自动 retry 为 0。/portal作为产品自然落点自动发起的只读 shell 请求须单独计数,不得误报为主动业务访问或伪造成 0。 --verify-password-changed必须证明 must-change=false、locks cleared、activeSessions=1、password-change audit 与本次 employee-smoke break-glass audit 均存在,且角色/权限不变量仍成立。- 最终 secret 原子更新且权限为
0600;临时凭据、浏览器状态和旧 hash 在验证通过后销毁,只记录清理布尔。 - 回到
08-08-production-authenticated-readiness后再执行员工合同权限负向验收;本任务成功不等于该负向验收已经通过。
R6 发布、证据与停止条件
- 本地代码必须先通过 focused tests、管理员脚本/repair helper 回归和只读 reviewer;reviewer 不改代码。
- 新脚本必须进入一个 clean/published、remoteVerified 的正常部署候选,经现有
deploy-existing.sh合同另行批准发布;发布前不得生产写入,发布失败立即停止。 - 生产执行前,运行镜像脚本 SHA-256 必须与获批候选中的脚本精确一致;不得复制或替换运行脚本。
- 生产发布与生产账号写入分别需要明确批准。任何审批、SHA、身份、migration、目标、权限、恢复材料或 secret 门禁失败均 fail-closed。
- 任务证据只记录 SHA match、枚举、布尔、计数、apply/login/change-password 次数和清理状态,不记录生产身份、完整路径、identifier、账号、密码、hash、token、cookie 或原始 payload。
Acceptance Criteria
- AC1:现有 repair helper 的完整 apply 副作用已由源码和 focused regression 固化;生产仅运行 dry-run,输出证明
passwordReset=false且 action 集合为空,helper apply 次数为 0。 - AC2:新脚本的 preflight 仅在唯一 active-employment/unarchived smoke employee、identifier ready、未锁定、must-change=false、
roles == ['employee']且四项管理权限全 false 时 PASS;所有非唯一/额外角色/权限扩大/状态漂移用例均 fail-closed。 - AC3:新脚本的 apply 只接受精确确认串和 stdin 密码;事务内二次核对目标/token/smoke/状态/精确角色/权限,且没有任何角色、权限、状态、identifier 或业务数据写入路径。
- AC4:focused tests 证明密码、锁、token、目标限定 session/challenge 与专用 audit 副作用;同时回归管理员 reset 与 repair helper,未扩大既有工具目标面。
- AC5:本地只读 reviewer 复核脚本与测试后通过;随后只在另行获批的正常发布中把脚本送入生产镜像,运行脚本 SHA 与 clean/published 候选精确一致,带外复制次数为 0。
- AC6:生产 preflight PASS 后 apply 恰好一次;明确结果或
APPLY_UNCERTAIN -> verify证明写入状态,审计计数写前/写后增量恰好 1,不存在第二次 apply。 - AC7:apply 后
--verify与独立只读快照均重新证明roles == ['employee']、四项管理权限全 false、目标唯一,且 must-change、locks、session/challenge、token/audit 副作用符合合同;未沿用写前结论。 - AC8:一次登录按预期进入强制改密,一次提交后
--verify-password-changedPASS;未主动访问业务功能且非认证 mutation 为 0,/portal自发只读请求独立计数,最终 secret 为0600,临时凭据、浏览器状态和旧 hash 已销毁。 - AC9:生产其他账号、角色/权限、企微绑定、合同、schema 与业务数据写入为 0;任务内无敏感值泄露,执行后只读 reviewer 和 task/process/governance/sensitive/diff 门禁通过。
- AC10:只向
08-08-production-authenticated-readiness交接最终员工凭据可用与负向前提仍成立;员工合同权限负向仍由该 task 执行,本任务不宣称产品已可告知用户测试。
Out of Scope
- 在本任务内执行合同列表/详情/终止入口或员工合同权限负向验收。
- 修改 employee 角色、权限、active/employment/archive 状态、identifier、企微绑定,创建新 smoke 账号或影响真人员工。
- 执行
repair-oa-only-smoke-accounts.js --apply,扩展管理员 break-glass 脚本到任意员工,或使用手写 SQL/Prisma、node -e、临时脚本复制完成写入。 - 启用邮件、SMTP、自助忘记密码、生产 feature flag、schema/migration 或合同数据。
- 把旧 hash 写回、删除审计或恢复历史 session/challenge;如确需此类写入必须另行批准。
Accepted Risks and Deferred Items
- 正常发布会重启/切换应用,即使候选只新增运维脚本也属于生产变更;因此发布审批独立于 task start 和账号写入审批。
- smoke 账号风险低于唯一管理员,但 reset 和强制改密仍会递增 token version、撤销目标会话并写审计,不能视为零成本操作。
- 旧 hash 仅保留一个格式正确的历史值,不能还原其他认证状态;apply 后不存在完整回滚,只能依赖 verify、最终改密或另行批准的 roll-forward。
- 员工负向验收和合同 UI 样本阻塞不由本任务收口;本任务完成后原 readiness task 仍可能保持 PARTIAL/PAUSED。
Open Questions
- 无。现有 helper 不具备密码重置能力,技术路径已收敛为新专用脚本;发布和生产写入在执行期分别走明确审批停点。
Test Plan
本地实现与复核
node --check server/src/scripts/reset-employee-smoke-password.jscd server && npm test -- tests/unit/scripts/reset-employee-smoke-password.test.js tests/unit/scripts/reset-active-admin-password.test.js tests/unit/scripts/repair-oa-only-smoke-accounts.test.jscd server && npm run prisma:validatecd client && npm run typecheck && npm run build(仅在形成可部署候选前执行)python3 ./.trellis/scripts/task.py validate 08-10-production-employee-smoke-break-glass-resetnode scripts/qa/check-process-contract.mjs --baseline --jsonnode scripts/qa/check-trellis-governance.mjs --jsonnode scripts/qa/check-sensitive-content.mjsgit diff --check
另行获批的生产阶段
- clean/published provenance、生产 identity/read-only preflight、65 migration、运行/公网 SHA、rollback window。
- 正常部署新候选;部署完成后运行镜像新脚本 SHA 与候选精确一致。
- repair helper 单次 dry-run(禁止 apply)→ 新脚本
--preflight→ 独立权限快照。 - 受控旧 hash、临时/最终 secret 就绪后单次
--apply EMPLOYEE_SMOKE_BREAK_GLASS→--verify→ 写后独立角色/权限快照。 - 一次登录/强制改密 →
--verify-password-changed→ secret/hash/browser 清理 → 最终 read-only preflight/window。 - 执行后只读 reviewer 核对脚本 SHA、apply 次数、审计增量、副作用、写后角色/权限与清理证据。
Evidence Rules
- 允许记录:候选/脚本 SHA、match 布尔、目标/审计/会话/challenge 计数、角色是否精确、权限布尔、attempt 次数、HTTP 状态、清理布尔。
- 禁止记录:identifier、email、phone、employee_no、name、目标 id、临时/最终密码、旧/新 hash、cookie/token、生产路径/身份/连接串和原始 payload。