归档内部OA运行恢复任务(07-26-internal-ops-recovery-baseline)
这个提交包含在:
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,150 @@
|
||||
# 内部 OA 运行与恢复底线设计
|
||||
|
||||
## 总体边界
|
||||
|
||||
本任务将现有四个底层脚本保留为单一职责工具,在其上增加一个成对备份入口和一个隔离演练入口。所有默认值面向本地/影子环境;真实环境只能沿 runbook 的人工门禁执行。
|
||||
|
||||
```text
|
||||
backup pair entry
|
||||
├─ backup-db.sh → <run-id>.db.dump
|
||||
├─ backup-storage.sh → <run-id>.storage.tar.gz
|
||||
└─ integrity checks → <run-id>.manifest
|
||||
|
||||
isolated drill
|
||||
├─ disposable PostgreSQL/database
|
||||
├─ disposable source/restore directories
|
||||
├─ synthetic marker + synthetic attachment
|
||||
└─ restore → compare → cleanup exact targets
|
||||
```
|
||||
|
||||
## 备份合同
|
||||
|
||||
- 一个批次只使用一个 `run_id`,由上层入口传递明确的 `OUT_FILE` 给现有底层脚本。
|
||||
- 先把产物写入批次临时目录;DB 和附件均成功并通过完整性检查后,再写 manifest 并把批次标记为完整。
|
||||
- manifest 使用稳定、脱敏的键:`schemaVersion`、`runId`、`createdAt`、`buildSha`(真实可知时)、`sourceImageDigest`(实测可知时)、两份相对文件名、字节数和 SHA-256;两类来源标识至少有一项。
|
||||
- 不把连接串、容器密码、存储源路径、真实业务文件清单或主机绝对路径写入 manifest。
|
||||
- 本任务不负责异机传输实现;runbook 要求操作者把完整批次复制到异机/异盘,并记录保留周期。
|
||||
|
||||
## DB 恢复安全合同
|
||||
|
||||
- 恢复前通过数据库连接读取 `current_database()`,不得只相信 URL 字符串或调用者描述。
|
||||
- 调用者必须提供与实际数据库名一致的确认值,并显式选择 `shadow`。
|
||||
- 实际目标库名还必须包含独立的 `shadow`、`drill` 或 `restore_test` 标记,不能仅靠调用者把模式写成 `shadow`。
|
||||
- 确认缺失、连接失败、数据库名不匹配或模式不是 `shadow` 时,在 `pg_restore --clean` 之前退出。
|
||||
- Docker fallback 也必须执行同样的数据库名探测和确认,不得成为绕过路径。
|
||||
- 真实/生产恢复不在本任务脚本默认路径内;未来如确需开放,应另建任务定义双人确认和事故编号。
|
||||
|
||||
## 附件恢复安全合同
|
||||
|
||||
- 必须显式传入恢复目标,不再默认落入试运行数据目录。
|
||||
- 明确拒绝仓库的默认试运行附件目录,即使该目录碰巧为空。
|
||||
- 影子恢复目标必须为空;非空时退出,不执行覆盖或合并。
|
||||
- 解压前列出归档成员并拒绝绝对路径、空成员和包含 `..` 路径段的成员。
|
||||
- 只对已验证的归档执行解压;演练通过内容 SHA-256 比较证明往返一致。
|
||||
|
||||
## 隔离演练
|
||||
|
||||
- 数据库只使用一次性本地容器或明确命名的 disposable database,插入不含业务信息的 marker 表/行。
|
||||
- 附件只使用临时目录和合成文本/二进制样件。
|
||||
- 演练记录开始/结束时间、各阶段通过状态和相对产物名;不记录连接串、容器密码或临时绝对路径。
|
||||
- 清理只针对演练创建的明确容器、数据库和临时目录;不得使用宽泛路径或工作区根目录。
|
||||
|
||||
## 兼容与回滚
|
||||
|
||||
- `backup-db.sh` 和 `backup-storage.sh` 的现有单独调用继续可用;推荐运维入口改为成对备份命令。
|
||||
- `restore-db.sh`/`restore-storage.sh` 的安全收紧属于有意的命令合同变化,runbook 和 checklist 必须同步更新。
|
||||
- 回滚代码可恢复旧脚本,但已经生成的 dump/tar 仍保持 PostgreSQL/tar 标准格式,不被新 manifest 锁定。
|
||||
- 如演练环境缺少 PostgreSQL 客户端和 Docker,停止 DB 动态演练;仍可完成 shell 静态检查和附件演练,不得转向真实环境补测。
|
||||
|
||||
## 外部门禁
|
||||
|
||||
以下事项不因本地演练通过而自动获批:读取真实运行环境、复制真实备份、建立异机定时任务、停机、恢复真实库/附件、部署和公网 `/ready`。进入这些步骤前再次列出目标、只读/写入范围和回滚方式并取得明确批准。
|
||||
|
||||
## 企微 HR 双 Secret 数据流
|
||||
|
||||
```text
|
||||
通讯录 Secret -> user/list_id -> userid 集合
|
||||
自建应用 Secret -> user/get + department/list + hr/get_staff_info
|
||||
-> normalize -> 脱敏 dry-run -> 既有 HR 同步写入
|
||||
```
|
||||
|
||||
- 保留 `WECOM_CONTACT_SECRET` 与 `WECOM_AGENT_SECRET` 的现有配置名和 token 来源审计,不增加第三套 Secret。
|
||||
- `listContactMembers` 分离列表 token 与详情 token;默认详情 token 等于列表 token 以保持内部调用兼容,但正式聚合入口明确传入自建应用 token。
|
||||
- 指定 userid 与全量 userid 两条路径使用同一详情 token 合同;部门列表也使用自建应用 token。
|
||||
- `/user/get` 失败仍沿用最小 userid 回退,HR 信息可补足资料;真实 dry-run 必须以归一化后的必填字段和冲突检查作为写入门,不因 API `errcode=0` 自动放行。
|
||||
|
||||
## 真实职责写入门
|
||||
|
||||
- 从管理员通讯录文件在本地内存中筛选两名目标,只把两人子集通过 stdin 送入生产 dry-run/apply;其余四人不进入生产输入、不创建账号,留待后续独立批次。
|
||||
- dry-run 和 apply 只输出两个匹配计数、身份是否不同、缺失字段数、冲突数和计划动作数量,不持久化筛选文件、完整路径或原始行。
|
||||
- 只有两个匹配计数均为 1、企微身份不同、均为 active、必填资料完整且手机号/邮箱不冲突时,才进入 apply。
|
||||
- 员工和企微绑定通过既有 HR master sync 服务创建或更新;随后通过既有角色写入语义分别设置 `employee + finance` 和 `employee + ceo`。
|
||||
- `finance` 的批准范围明确包含 `payroll.manage`、`employee.bank.read`、财务付款和付款批次管理,不包含 `asset.transfer`;不以 `finance_mgr` 解决 CEO 本人报销兜底。
|
||||
- 角色变更递增 `token_version`、撤销活动会话并写 `SYSTEM_USER_ROLES_SET` 审计;最终只核对数量、角色集合、不同 employee id、角色待办计数变化、审计存在和付款业务零写入。
|
||||
- maker-checker 只验证付款批次创建人不能复核同一批次,不宣称费用审批与财务处理完全分离;生产不创建测试付款批次,使用合成路由测试和生产只读身份/权限证据。
|
||||
- 回滚是通过相同角色接口恢复两人的写入前角色集合,并再次撤销会话、保留审计;不得通过整库恢复回退角色。
|
||||
- 任一门禁失败即停止,不进行部分角色写入;如员工同步已经成功而角色阶段失败,保留同步审计并立即报告,不用删除真实员工记录进行破坏性回滚。
|
||||
|
||||
## 企微本人敏感信息授权边界
|
||||
|
||||
- 本轮生产身份建档不走本人授权,而走管理员通讯录两人子集。以下 OAuth 合同继续保留为已实现、默认关闭的未来补资料能力,本轮验收标记为不适用,不要求成员点击授权链接。
|
||||
|
||||
- `/user/get` 和人事助手批量读取继续只负责 userid、姓名、部门等可读字段;不得把批量缺字段误判为企微完全不支持手机号和邮箱。
|
||||
- 2022-06-20 后新建自建应用的敏感字段走独立 OAuth 数据流:管理员在应用详情选择字段,成员以 `snsapi_privateinfo` 手工确认,`getuserinfo` 返回短时 `user_ticket`,服务端立即调用 `/cgi-bin/auth/getuserdetail`。
|
||||
- `user_ticket`、授权 code 和完整敏感响应不得写日志、任务证据或审计 extra;只保存业务所需的规范化手机号/邮箱及字段存在性审计。
|
||||
- 普通 `snsapi_base` 登录与敏感资料授权分开,避免每次登录引入手工授权;用户已批准未绑定的 active 可见成员通过独立入口自助创建仅授予 `employee` 的 OA 账号。
|
||||
|
||||
### OAuth 开通数据流
|
||||
|
||||
```text
|
||||
GET /auth/wecom/login?mode=onboard
|
||||
-> feature flag + state(mode=onboard)
|
||||
-> authorize(scope=snsapi_privateinfo)
|
||||
-> getuserinfo(code) -> userid + user_ticket
|
||||
-> getuserdetail(user_ticket) -> mobile + email|biz_mail
|
||||
-> user/get + department/list -> name + active + department
|
||||
-> unique precheck -> transaction(employee + employee role + binding + audit)
|
||||
-> authenticated session
|
||||
```
|
||||
|
||||
- `mode=login|bind` 保持既有 `snsapi_base` 和行为;新增 `mode=onboard` 才使用 `snsapi_privateinfo`。
|
||||
- `getuserinfo` 只把 `user_ticket` 传给同一请求中的详情交换函数,诊断信息只记录字段是否存在。
|
||||
- 详情中的 userid 必须与身份接口 userid 一致;邮箱优先使用成员授权的个人邮箱,无个人邮箱时使用企业邮箱,二者都缺失则停止。
|
||||
- 联系人详情必须证明成员 active,并提供姓名、部门;手机号/邮箱与不同 OA 员工冲突时停止,不自动合并。
|
||||
- 已有绑定员工只补齐经授权的规范化资料且不改角色;新员工在单事务内创建 employee、`employee` 角色、企微绑定和脱敏审计。任何一步失败均不留下部分账号。
|
||||
- 自助开通不替代管理员职责分配;`finance`、`ceo` 仍沿用角色接口的 token version、会话撤销和审计合同。
|
||||
|
||||
## 企微通讯录导出与手机号可空
|
||||
|
||||
- 管理员导出的 XLSX 只作为一次性受控输入,不进入 Git、Trellis、报告或生产文件系统;CLI 支持从 stdin 读取,默认仅 dry-run。
|
||||
- 入口识别企微官方说明行和中文表头,将姓名、账号、职务、部门、手机、邮箱/企业邮箱、激活/禁用状态归一化为既有 HR master profile,再复用部门解析、员工匹配、企微绑定、审计和 `employee` 初始角色语义。
|
||||
- `Employee.phone` 改为 nullable unique:缺失值统一保存为 `NULL`,非空值继续唯一。企业邮箱仍必填且唯一,企微账号仍是绑定主键;不生成占位手机号。
|
||||
- 来源未提供手机号时,不得把既有 OA 手机号清空;来源提供手机号时才进入变更和唯一性检查。本人授权同样允许手机号缺失,但邮箱或企业邮箱仍必须有效。
|
||||
- apply 需要命令参数和独立环境变量双门,且真实运行另行批准;dry-run 与 apply 的报告都只含计数、动作、冲突类别和脱敏摘要。
|
||||
- 本轮在内存中筛选两名目标后再送入 stdin;筛选结果不落盘,目标外四人不进入 dry-run/apply,也不因出现在原始导出文件中而获得 OA 活跃账号。
|
||||
- 生产迁移回滚前必须确认不存在 `phone IS NULL`;否则停止回滚,不用假数据填充。
|
||||
|
||||
## 部署目录统一设计约束
|
||||
|
||||
- 现场是混合部署,不能把“统一目录”处理成普通 rsync:必须以当前活动 PostgreSQL 数据和附件挂载为唯一数据事实源。
|
||||
- 在停机切换前固定记录活动容器、数据挂载、环境文件摘要、端口、Caddy 路由和回滚镜像;不得打印环境变量内容。
|
||||
- 现场通过非默认宿主端口提供 OA;部署脚本必须从已验证的 Compose/env 配置解析实际端口,不能回退到硬编码(本地已实现,待获批部署)。
|
||||
- 两套目录的 Compose 容器名和环境文件不同。切换前必须先选择唯一目录并证明其 DB/附件目标就是当前活动数据;不得让 Compose 创建第二套空数据库后误判部署成功。
|
||||
- 统一后应由同一目录负责 source sync、Compose、环境文件和运维 timer,并让 `/ready`、前端 marker 和容器环境同时暴露同一短构建标识。
|
||||
- 任一迁移状态、数据挂载、端口或公网构建标识不匹配时,立即切回旧容器和 Caddy 目标;不执行数据库 reset 或数据目录覆盖。
|
||||
- Compose 不再固定 `container_name`;deploy 与 timer 通过 `-p` 显式选择同一项目,PostgreSQL 与附件宿主路径分别由 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 参数化。
|
||||
- 项目名隔离不是独立部署步骤。项目名、容器身份和数据目录必须在同一次停机切换中启用;旧实例未停止时不得让新 PostgreSQL 挂载同一 PGDATA。
|
||||
- DB 不发布宿主端口;应用仅通过 Compose 内部网络访问 `db`。环境契约必须拒绝 DB `ports`、固定容器名或未参数化数据目录回退。
|
||||
- Docker 备份/恢复必须显式传入由 `compose ... ps -q db` 得到的已核验容器 ID;附件备份必须显式传入活动挂载源,两个脚本都不猜默认值。
|
||||
- 远端 `/ready` 构建标识在 server 容器内用 Node 校验,宿主机只负责 curl 和 Compose,不要求安装 Node。
|
||||
- `deploy.sh` 不解析或 source 远端 `.env` 来猜宿主路径;调用者必须在发布命令中显式传入受限字符集的绝对 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 和数据模式。`existing` 要求目录已存在且 PGDATA 含 `PG_VERSION`;`initialize` 才允许创建空目标。
|
||||
- 本地直接 Compose 可继续使用示例相对默认值,但正式发布入口拒绝相对路径,从而避免项目名变化时 Docker 以 root 静默创建第二套空目录。
|
||||
- manifest 的 `buildSha` 优先使用调用者显式值,否则仅在本地 Git 工作树存在时推导;另以可选 `sourceImageDigest` 记录活动镜像不可变标识。旧实例 build SHA 不可知时只记录实测 digest,不填写目标提交。
|
||||
|
||||
### 生产统一顺序
|
||||
|
||||
1. 另行获批后,从活动 DB 和活动附件挂载源重新生成成对异机备份作为灾难回滚点;旧批次因混入非活动附件目录只保留审计,并记录活动数据挂载、环境摘要、容器镜像和 Caddy 端口。
|
||||
2. 本地先修复 Compose 可配置回环端口及部署脚本动态端口探测,通过全部门禁并提交、发布批准提交。
|
||||
3. 停机后把活动 PGDATA 和附件复制到新的显式绝对目录,复制前目标必须为空;保留旧目录原位且不再挂载写入,直到观察期结束。复制仅在旧 PostgreSQL 完全停止后执行,并保留所有权、权限、链接和扩展属性。
|
||||
4. 用新 Compose 重建 db/init/server,应用已提交 migrations;旧容器和旧目录在验收前保留,不清理。
|
||||
5. 公网构建标识、数据计数和关键部署只读检查通过后更新运维 timer;角色与企微入口仍按各自真实写入门单独验收,不阻塞部署运维基线收口。部署检查失败则停止新容器并恢复旧目录/容器和原 Caddy 目标。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,111 @@
|
||||
# 内部 OA 运行与恢复底线执行计划
|
||||
|
||||
## 实施步骤
|
||||
|
||||
- [x] 审计现有备份/恢复脚本、部署卷、runbook 和 checklist,冻结现有兼容入口与风险点。
|
||||
- [x] 增加 DB + 附件成对备份入口、统一批次号、完整性检查、SHA-256 和脱敏 manifest。
|
||||
- [x] 收紧 DB 恢复:实际数据库名探测、影子模式和精确确认必须在破坏性命令前通过。
|
||||
- [x] 收紧附件恢复:显式目标、空目录门禁和 tar 路径安全检查。
|
||||
- [x] 增加隔离合成数据演练,覆盖成功往返及关键负向场景。
|
||||
- [x] 更新 runbook/checklist 和部署规约,区分普通备份、影子演练和真实事故恢复。
|
||||
- [x] 运行任务测试计划和 Trellis 治理门禁;按实际结果记录本地 RTO,不伪造 `/ready` 或异机证据。
|
||||
- [x] 在单独授权后完成真实运行来源、职责分离、异机副本和 `/ready` 的只读核查,并如实记录 `NO_GO` 缺口。
|
||||
- [x] 经明确授权生成并复制过 DB+附件+manifest 批次;后续发现附件来自非活动目录,已将该批次降级为无效恢复证据并保留审计。
|
||||
- [x] 经后续明确授权,从活动 DB 与活动 server 附件挂载源重新生成正确批次并完成异机复制与目标端复验。
|
||||
- [x] 经后续明确授权,使用正确异机批次完成真实备份格式的 DB+附件隔离影子恢复,并逐项复验迁移数、关键表计数和附件数量。
|
||||
- [x] 经后续明确授权完成真实职责分配与部署/迁移,并在部署环境复验应用 `/ready`、构建标识和数据连续性。
|
||||
- [x] 修复企微 HR 双 Secret 分工:通讯录 token 只列 userid,自建应用 token 读取详情、部门和 HR 信息。
|
||||
- [x] 补充单元测试并运行后端语法、聚焦测试、Prisma 与 Trellis 门禁。
|
||||
- [x] 在真实环境执行两名目标成员的脱敏 dry-run;两人均唯一、不同身份且 active,但批量链路均缺手机和邮箱,已按门禁停止真实写入。
|
||||
- [x] 复用既有 HR 同步与角色语义写入 `employee + finance`、`employee + ceo`,复核会话失效、审计和 maker-checker;角色阶段未附带部署或迁移。
|
||||
- [x] 增加默认关闭的一次性企微敏感资料授权入口,保持普通登录静默;实现 `user_ticket -> getuserdetail`、active/字段/唯一性门禁和仅 `employee` 自助开通。
|
||||
- [x] 补齐 OAuth 路由、服务、配置契约和单元测试,证明票据/原始敏感响应不进入日志、Cookie、数据库或审计。
|
||||
- [x] 修复部署脚本的现场端口解析,确定唯一部署目录,并在活动 DB/附件挂载、环境摘要和旧镜像回滚点全部确认后执行获批切换。
|
||||
- [x] 完成本地 Compose/备份加固:移除固定容器名和 DB 宿主端口,参数化数据目录,deploy/timer 显式 project,备份来源缺失硬失败,远端构建标识改由容器内 Node 核验。
|
||||
- [x] 增加隐式附件源、隐式 DB 容器、DB 端口与固定容器名回退的负向契约演练;未部署或启用新 Compose。
|
||||
- [x] 收紧正式 deploy 数据路径:显式绝对路径、`existing|initialize` 模式、既有 PGDATA 标记和禁止 Docker 静默建空目录。
|
||||
- [x] 让成对 manifest 尊重显式 `BUILD_SHA` 并支持活动镜像 digest;补 Node/Python 等价与非法来源标识负向测试。
|
||||
- [x] 把旧 Compose project 实测、镜像 digest、58 项迁移与关键表计数、影子逐表/附件计数验收、停机后复制并保留旧目录写入 runbook/checklist/spec。
|
||||
- [x] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
|
||||
- [x] 本轮不适用/延后:不启用企微敏感资料 OAuth 开关,不要求两名目标点击一次性授权;入口保持默认关闭,未来仅用于本人补资料。
|
||||
- [x] 从管理员通讯录在内存中筛选两名目标,先执行生产脱敏 dry-run;其余四人不进入输入、不创建账号,另轮处理。
|
||||
- [x] dry-run 零冲突后另行批准 apply,确认两人账号/企微绑定唯一且初始角色严格为 `employee`,不持久化通讯录文件。
|
||||
- [x] 记录两人原角色、活跃会话、角色待办和审计基线,再分别写入 `employee + finance` / `employee + ceo`;明确 `finance` 含薪资/银行资料权限且不含 `asset.transfer`,不授予 `finance_mgr`。
|
||||
- [x] 复核角色接口递增 token version、撤销会话和 `SYSTEM_USER_ROLES_SET` 审计;以合成测试证明付款批次 maker-checker,不在生产创建测试付款数据,也不把结论扩大为费用审批完全分离。
|
||||
- [x] 回滚合同固定为只通过相同角色接口恢复两人原角色集合并复核会话/审计,不恢复整库;本次写入和验证均成功,未触发回滚。CEO 既有角色待办可见性变化及本人无主管报销继续阻断已记录。
|
||||
- [x] 将 `Employee.phone` 改为 nullable unique,并同步员工创建/批量导入、企微 HR master、本人授权、登录查询和前端空值合同;不执行生产 migration。
|
||||
- [x] 增加企微通讯录 XLSX/stdin 默认 dry-run 入口,复用 HR master 写入语义,apply 使用独立双门且本阶段不执行。
|
||||
- [x] 用完全合成的通讯录覆盖“5 个手机号 + 1 个缺失”、重复账号/手机号/邮箱、说明行/中文表头、既有手机号不被空来源清除和报告隐私门禁。
|
||||
|
||||
## 推荐文件边界
|
||||
|
||||
- 复用并按需修改:`scripts/backup-db.sh`、`scripts/backup-storage.sh`、`scripts/restore-db.sh`、`scripts/restore-storage.sh`。
|
||||
- 最多新增两个小脚本:一个成对备份入口、一个 QA-only 隔离演练入口;不创建通用备份框架。
|
||||
- 更新现有 `deploy/trialrun/runbook.md`、`deploy/trialrun/checklist.md` 和 `.trellis/spec/hyb-oa/deployment-trialrun.md`,不新建常驻平行业务文档。
|
||||
- 优先修改 `server/src/routes/wecomAuth.js` 并抽取小型企微本人授权/员工开通服务;不把 HR 批同步服务改成通用身份平台。
|
||||
- 更新现有 `docs/企微API能力目录.md` 和 `.trellis/spec/hyb-oa/wecom-boundary.md`,记录批量读取与本人 OAuth 的稳定边界。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `bash -n scripts/backup-db.sh scripts/backup-storage.sh scripts/restore-db.sh scripts/restore-storage.sh scripts/backup-all.sh scripts/qa/run-backup-restore-drill.sh`
|
||||
- `bash scripts/qa/run-backup-restore-drill.sh`
|
||||
- `node deploy/trialrun/validate-env-contract.js`
|
||||
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-26-internal-ops-recovery-baseline`
|
||||
- `node scripts/qa/check-trellis-governance.mjs`
|
||||
- `node scripts/qa/check-sensitive-content.mjs`
|
||||
- `git diff --check`
|
||||
- `cd server && node --check src/services/integrations/wecomHrMaster.js`
|
||||
- `cd server && npm test -- --run tests/unit/wecomHrMaster.service.test.js`
|
||||
- `cd server && npm run prisma:validate`
|
||||
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js`
|
||||
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js tests/unit/wecomOAuthOnboarding.service.test.js`
|
||||
- `cd server && npm test -- --run tests/unit/routes/users.import.test.js tests/unit/wecomHrMaster.service.test.js tests/unit/wecomOAuthOnboarding.service.test.js tests/unit/scripts/import-wecom-directory-export.test.js`
|
||||
- `bash -n deploy/trialrun/deploy.sh`
|
||||
- `bash -n deploy/trialrun/install-warch3-ops-timer.sh`
|
||||
|
||||
## 本轮本地验证证据(2026-07-26)
|
||||
|
||||
- 后端全量 Vitest:162 个测试文件、1000 个测试通过;安全补修后企微聚焦回归为 3 个文件、30 个测试通过。
|
||||
- `prisma validate`、四个企微改动文件 `node --check`、部署环境契约和部署脚本语法均通过。
|
||||
- 本地实现没有开启 `WECOM_OAUTH_ONBOARDING_ENABLED`,没有部署、迁移、写真实员工/角色或修改企微后台权限。
|
||||
|
||||
## 本地部署加固证据(2026-07-27)
|
||||
|
||||
- Shell 语法、部署环境契约、Compose 示例配置解析、Trellis task/governance、敏感扫描、文档注册、流程合同、Prisma 和 `git diff --check` 通过。
|
||||
- 合成备份恢复在 Node 路径和移除 Node 的 Python 3 回退路径均通过,最近两次本地参考 RTO 均为 6 秒;负向覆盖隐式附件源、隐式 DB 容器、DB 宿主端口、固定容器名和 deploy 缺失 project name。
|
||||
- 本阶段授权仅覆盖本地文件、测试、规约和提交,不包含生产备份、停机、迁移、部署、timer 安装或企微写入。
|
||||
- 已确认旧异机批次混用了活动 DB 与非活动附件目录,保留审计但不再作为活动实例成对恢复证据;正确批次须另行批准重做。
|
||||
- 复核补修后,正式 deploy 的相对/重叠路径负向门禁、显式来源标识、Node 路径以及无 Node/无 Git 的 Python 3 + digest-only 路径均通过;最近两次合成 RTO 均为 7 秒。
|
||||
- 已从活动 DB 容器与活动 server 附件挂载生成正确批次并复制到 ARM 备份机;DB catalog、manifest、源/目标产物 SHA-256、附件 tar 与 0 个普通文件计数均通过,且未伪造旧实例 build SHA。ARM 只承担异机备份与恢复演练职责,不运行 OA 服务。
|
||||
- 已在 ARM 备份机用不发布端口的一次性 PostgreSQL 16 容器恢复正确批次:58 项成功迁移、12 张关键表计数和 0 个附件普通文件均与冻结基线一致,包含镜像拉取的实测 RTO 为 33 秒。演练后容器、临时附件目录和本轮新增镜像均已删除,未运行 OA server;应用 `/ready` 留待生产部署环境验证。
|
||||
|
||||
## 手机号可空与通讯录导入本地证据(2026-07-27)
|
||||
|
||||
- 后端全量 Vitest:163 个测试文件、1016 个测试通过;补充双 apply 门断言后,通讯录导入聚焦 8 项再次通过。新增覆盖普通用户创建/显式清空、CSV 空手机号、HR 空来源保留既有值、OAuth 无手机号建档、通讯录说明行/中文表头、企业邮箱门禁、非法手机号、重复字段、apply 双门及脱敏状态。
|
||||
- `prisma validate`、四个后端改动文件语法、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。
|
||||
- 管理员提供的真实通讯录仅执行解析级只读检查:6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库、未执行 apply、未分配角色、未发送企微消息,文件保持 Git 排除。
|
||||
- 最终只读复核确认 migration 仅删除手机号 `NOT NULL`、唯一索引保留;导入及本人开通只创建 `employee`,不自动授予 `finance`、`ceo`、`admin` 或 `system_admin`;空来源更新省略 phone 字段,真实文件名、完整路径和个人字段未进入差异。
|
||||
- 获批生产脱敏 dry-run 只接收内存筛选的两人子集,结果为输入 2、active 2、不完整 0、计划创建 2、更新 0、冲突/跳过/失败均为 0;两名目标均有手机号和企业邮箱。dry-run 未写库、未创建账号、未分配角色,目标外四人未进入输入。
|
||||
- 生产 Node 对 Promise 版 `readFile(0)` 返回 `ERR_INVALID_ARG_TYPE`,正式 CLI 在解析前安全失败;本地改为 `fs.readFileSync(0)` 并增加 fd 读取回归。生产证据先由同一解析、同步和报告逻辑的 fd 兼容只读入口取得;修复随 `4886f9b` 部署后,正式 CLI 再次得到相同结果,本阶段未执行 apply。
|
||||
|
||||
## 生产部署与 timer 收口证据(2026-07-27)
|
||||
|
||||
- 已在获批停机窗口把生产切换到唯一 Compose project;活动 DB 与附件从停机冻结副本进入新的独立目录,旧容器保持停止,旧数据目录原位保留作回滚点。
|
||||
- 生产已应用 61 项 migration;12 张关键表计数与冻结基线一致,活动附件普通文件数仍为 0。server 仅回环发布,DB 不发布宿主端口。
|
||||
- 容器内与公网 `/ready`、前端 marker 均返回批准提交的同一 12 位构建标识;runtime smoke、external-deps smoke 和公网 bundle/marker 检查通过。
|
||||
- 运维 timer 安装器已要求显式绑定 Compose project、活动 PGDATA、活动附件目录、完整后端 SHA 和 12 位前端 SHA。生产 timer 已重新安装并保持 enabled/active,daily service 未被手工触发;unit 环境与活动挂载/构建标识一致,同环境 Compose config 与 server exec 解析通过。
|
||||
- timer 首次收口时发现从 deploy 复制的旧路径正则会误拒绝所有绝对路径;安装在写 unit 前安全停止。现已与 deploy 的根目录、尾随斜杠、双斜杠及点路径段门禁保持一致,并由环境契约阻止旧正则回退。
|
||||
- 本轮没有启动真实告警任务,没有修改员工、角色、付款、企微后台或企微开通开关;职责分离与两人本人授权继续保持未完成。
|
||||
- `4886f9b` 已通过正式部署入口发布:来源 published/remoteVerified,62 项 migration 无待应用项,容器与公网 `/ready`、external deps smoke、runtime smoke、公网前端构建 marker 和资源扫描通过;生产环境文件未覆盖。部署后的通讯录正式 CLI dry-run 为输入 2、计划创建 2、冲突/跳过/失败 0,未写员工、绑定或角色。
|
||||
- 写入前自动备份、ARM dead-man 和管理员 actor 门禁通过。两人 apply 创建 2、成功 2;写后为 2 名 active、2 个不同企微绑定、初始角色均仅 `employee`。角色接口写入后 token version 合计递增 2、活动会话 0、`SYSTEM_USER_ROLES_SET` 审计新增 2、CEO 待办 0→0、付款批次数量变化 0。
|
||||
- 最终生产只读权限核对通过;本地 `permissions` 与 `paymentBatches.reimbursement` 聚焦回归共 40 项通过,maker-checker 只由合成测试验证,未产生生产测试付款。目标外四人未进入本轮输入;本次未触发角色回滚。
|
||||
|
||||
## 回滚点
|
||||
|
||||
- 成对入口失败:保留四个底层脚本,不写完整 manifest。
|
||||
- DB/附件安全门禁与现有操作冲突:先修 runbook 和调用方,不放宽默认门禁。
|
||||
- 动态演练环境不可用:记录缺失依赖并停止,不使用真实数据替代。
|
||||
- 任何命令解析到生产、远程或含真实数据的目标:立即停止并请求明确授权。
|
||||
- 真实 dry-run 任一目标非唯一、身份相同、字段缺失或存在唯一约束冲突:停止 apply,不尝试猜测身份。
|
||||
- 员工同步后角色写入失败:不删除真实员工或企微绑定;记录脱敏结果并停止,待修复后幂等重试角色阶段。
|
||||
- 现场 Compose 解析出第二套 DB/附件目标、意外容器名或非活动端口:停止部署,不创建或迁移数据。
|
||||
@@ -0,0 +1,137 @@
|
||||
# 内部OA运行与恢复底线
|
||||
|
||||
## Goal
|
||||
|
||||
为 5–10 人内部 OA 建立一个主要开发者可执行的备份与恢复底线:数据库和附件始终成对备份、产物可校验、恢复默认只能进入明确的影子目标,并用合成数据证明可恢复;同时把真实内部实例的运行来源、角色职责、异机副本和 `/ready` 验收留在明确的外部授权门后。
|
||||
|
||||
## Background
|
||||
|
||||
- 当前可信状态仍是 `LOCAL_CANDIDATE_PASS`,不能据此宣称已部署或可以正式内用。
|
||||
- 已有 `backup-db.sh`、`backup-storage.sh`、`restore-db.sh` 和 `restore-storage.sh`,runbook 也描述了 DB + 文件恢复,但尚无当前恢复演练证据。
|
||||
- DB 恢复会执行 `pg_restore --clean --if-exists`;当前脚本没有影子目标确认,误指向真实库会产生破坏性后果。
|
||||
- 文件恢复会直接解压到目标目录;当前不会拒绝非空目录,可能把旧文件和恢复文件混合。
|
||||
- DB 与附件备份分别生成时间戳,没有共同批次清单、完整性校验和,无法稳定证明两份产物属于同一次恢复点。
|
||||
- 本任务以小团队低维护为目标,不引入备份平台、对象存储、Kubernetes、云厂商锁定或复杂灾备编排。
|
||||
|
||||
## Scope
|
||||
|
||||
- 本地备份/恢复 shell 脚本的安全合同、成对备份入口、完整性校验与脱敏 manifest。
|
||||
- 使用一次性 PostgreSQL 目标和临时附件目录完成合成数据恢复演练及负向测试。
|
||||
- 更新现有试运行 runbook、checklist 和部署规约,明确影子恢复与真实恢复边界。
|
||||
- 规划并保留真实运行 commit、访问边界、角色职责、异机副本及恢复后 `/ready` 的外部授权门;未授权前不执行这些动作。
|
||||
- 经用户逐项授权后,修正企微通讯录与人事助手的双 Secret 读取分工,并用脱敏 dry-run 唯一确认两名既有企微成员。
|
||||
- 唯一匹配且必填资料、冲突检查通过后,将两名成员同步为 OA 员工,并分别配置 `employee + finance` 与 `employee + ceo`,形成不同自然人的付款制单/复核职责。
|
||||
- 增加独立的一次性企微敏感资料授权入口:普通登录继续使用 `snsapi_base`,首次开通使用 `snsapi_privateinfo`;未绑定的在职可见成员授权成功后只自动获得 `employee` 角色。
|
||||
- 支持管理员提供的企微通讯录 XLSX 通过 stdin 做脱敏 dry-run,并允许个别成员暂缺手机号;企业邮箱、企微账号、姓名和部门仍作为建档门禁。
|
||||
- 本轮通讯录导入只筛选两名已具备职责验收计划的目标成员;其余四名成员不进入 dry-run apply 输入、不创建活跃账号,明确留待后续独立批次。
|
||||
- 修复部署脚本与现场端口、目录、容器和构建标识契约,统一真实运行来源;切换前保留活动数据、环境文件和旧容器回滚点。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 提供一个简单的成对备份入口,复用现有 DB/附件脚本,以同一批次标识产生两份产物和脱敏 manifest。
|
||||
- manifest 只包含批次号、相对文件名、大小、SHA-256、创建时间及安全的版本信息,不包含数据库 URL、Secret、真实附件名或完整绝对路径。
|
||||
- 备份完成前验证 DB dump 可列出、附件 tar 可读取;任一部分失败时不得产出“完整批次”结论。
|
||||
- DB 恢复必须先解析并显示目标数据库名,并要求调用者显式确认相同目标;默认仅允许 `shadow` 演练模式,且实际库名必须带 `shadow`、`drill` 或 `restore_test` 独立标记。本任务不开放生产恢复快捷开关。
|
||||
- 附件恢复必须要求显式目标目录;默认拒绝非空目录,并在解压前检查归档条目,禁止绝对路径和 `..` 越界路径。
|
||||
- 用完全隔离的合成数据库和合成附件完成本地往返演练,不读取或复制真实发票、合同、银行资料或生产数据。
|
||||
- 恢复演练至少证明:DB 标记数据恢复、附件内容哈希一致、损坏备份被拒绝、错误确认值被拒绝、非空附件目标被拒绝。
|
||||
- 更新现有 runbook/checklist,使正常备份、影子演练、真实恢复三种场景不再共用含糊命令;保持现有本地存储默认,不启用 MinIO。
|
||||
- 将真实内部实例核查列为后续同任务外部门禁:运行 commit/访问方式/TLS、真实职责分配、异机副本和恢复后 `/ready` 均需另行批准和脱敏留证。
|
||||
- 企微通讯录 Secret 只用于 `/user/list_id`;自建应用 Secret 用于 `/user/get`、`/department/list` 和获授权的人事助手 `/hr/get_staff_info`,不得回退到已废弃的人事助手 Secret。
|
||||
- 真实同步前只输出匹配数量、身份是否不同、缺失字段数、冲突数和计划创建/更新数,不输出姓名、userid、手机号、邮箱、部门原文或原始 payload。
|
||||
- `Employee.phone` 允许为 `NULL`,但不得写空字符串或假手机号;非空手机号继续保持唯一。企微账号和企业邮箱必须非空且唯一,缺手机号成员使用企微或邮箱登录,后续可由本人授权或管理员补齐。
|
||||
- 真实写入必须复用现有员工同步、角色、会话撤销和审计语义;财务处理人与 CEO 复核人必须是两个不同企微身份,且不得把 `admin` 当作普通业务复核角色。
|
||||
- 目标角色固定为 `employee + finance` 与 `employee + ceo`,不授予 `finance_mgr`。接受 `finance` 同时具备 `payroll.manage` 和 `employee.bank.read`,意味着财务处理人可访问薪资和员工银行账号;`finance`、`finance_mgr` 均不得拥有 `asset.transfer`。
|
||||
- 本轮 maker-checker 验收只针对付款批次复核:创建人复核同一批次必须被 `maker_checker_conflict` 拒绝;不得把结论表述为费用审批、财务付款或全部财务操作已完全按自然人分离。
|
||||
- 敏感资料授权必须显式受 `WECOM_OAUTH_ONBOARDING_ENABLED` 控制,默认关闭;成员必须在应用可见范围、企微状态 active,并至少实际同意邮箱或企业邮箱授权;手机号可暂缺,但返回时必须规范化并参与唯一性检查。
|
||||
- `user_ticket` 只在服务端内存中用于一次 `getuserdetail` 调用,不写日志、Cookie、数据库、审计或错误详情;授权 code 和原始敏感响应同样不得持久化。
|
||||
- 自助开通按企微 userid、非空手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
|
||||
- 本轮真实建档改用管理员导出的通讯录两人子集:先以 stdin dry-run/apply 创建或更新 employee 与企微绑定,且新账号严格只有 `employee`;再由管理员单独写入附加角色。一次性 `snsapi_privateinfo/getuserdetail` 入口保持默认关闭,仅作为未来本人补资料能力,不作为本轮生产验收门禁。
|
||||
- 角色写入前后记录目标原角色集合、不同 employee 身份、对应角色待办计数、活跃会话数量和审计计数;回滚只把两人恢复为写入前角色集合,复用 token version、会话撤销和审计,不恢复整库。
|
||||
- CEO 角色会让既有 `assignee_role=ceo` 待办立即可见,并影响新提交生成的角色节点,但不重写既有审批快照,也不恢复过去失败的提交。CEO 本人无主管且没有 active 非本人 `finance_mgr` 时,报销保持阻断,本轮不通过扩权绕过。
|
||||
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
|
||||
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
|
||||
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
|
||||
- 正式 `deploy.sh` 必须由调用者显式提供绝对 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 和 `DEPLOY_DATA_MODE=existing|initialize`;既有实例默认只接受包含 `PG_VERSION` 的现存 PGDATA,不得让 Docker 静默创建空目录。
|
||||
- 成对备份必须尊重调用者显式提供的真实来源标识。当前旧实例没有 build SHA 时记录活动镜像不可变 digest,不得用待部署提交冒充备份来源。
|
||||
- 部署健康检查必须从 Compose 解析实际宿主端口,并校验后端 `/ready`、前端 marker 与批准提交一致;应用已提交的 Prisma migrations 但禁止 reset 或改写历史。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 一条命令生成同批 DB、附件和脱敏 manifest,且完整性校验通过。
|
||||
- [x] DB 恢复在缺少或不匹配目标确认时失败,不会执行 `pg_restore --clean`。
|
||||
- [x] 附件恢复拒绝隐式目标、非空目录和危险 tar 路径。
|
||||
- [x] 隔离合成数据演练证明 DB 与附件可往返恢复,并记录实际耗时作为本地 RTO 参考。
|
||||
- [x] runbook/checklist 明确影子演练命令、生产停止条件、DB + 附件配对要求和异机副本要求。
|
||||
- [x] 相关 shell 语法、QA 脚本、Trellis 治理、敏感扫描和 `git diff --check` 通过。
|
||||
- [x] 在获得单独授权后,确认真实运行 commit、访问边界、职责分离、异机备份及恢复后 `/ready`;未授权前保持未勾选且不宣称正式可用。
|
||||
- [x] 双 Secret 调用测试证明成员 ID 由通讯录 token 读取,成员详情、部门和 HR 资料由自建应用 token 读取。
|
||||
- [x] 真实脱敏 dry-run 证明两个目标各唯一匹配、身份不同、必填资料和唯一约束无阻塞。
|
||||
- [x] 获批写入后,两名活跃员工分别具有 `employee + finance` 与 `employee + ceo`,企微绑定不同,角色审计存在且未产生付款业务写入。
|
||||
- [x] 普通企微登录仍生成 `snsapi_base`,一次性开通入口生成 `snsapi_privateinfo`;缺少开关、`user_ticket`、成员授权字段、active 状态或唯一性时均拒绝创建(本地 synthetic 验证)。
|
||||
- [x] 本轮不适用:生产不启用 `snsapi_privateinfo/getuserdetail` 本人授权,OAuth 入口保持默认关闭并延后为补资料能力;真实建档改用企微通讯录两人子集,仍要求新账号角色严格等于 `employee`,且不持久化通讯录文件或原始行。
|
||||
- [x] 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 `/ready` 和前端 marker 返回同一短构建标识。
|
||||
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
|
||||
- [x] 部署入口拒绝隐式/相对数据目录及缺少数据模式,成对 manifest 可记录显式 build SHA 或活动镜像 digest,并有 Node/Python 等价测试。
|
||||
- [x] 两名目标成员经企微通讯录两人子集 dry-run/apply 后账号唯一、企微绑定不同且初始角色仅为 `employee`,再由管理员写入 `employee + finance` 与 `employee + ceo`;其余四人保持未导入,付款批次 maker-checker、会话撤销和角色审计通过。
|
||||
- [x] 本地合成企微导出证明 5 人有手机号、1 人手机号为空时 dry-run 可通过;空值保存为 `NULL`,非空手机号/邮箱/企微账号重复均被拒绝,报告不含个人字段原文。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不读取真实附件正文,不执行生产恢复、数据库 reset、迁移历史改写或付款/报销业务写入。
|
||||
- 除已批准的 `Employee.phone` nullable 兼容外,不扩大修改其他 Prisma schema、权限模型、审批链或付款状态机;公开用户 API 仅同步手机号可空合同,并通过既有角色模型写入获批职责。
|
||||
- 不引入 MinIO、云备份 SDK、全量灾备平台、自动跨地域复制或 24×7 值守。
|
||||
- 不在仓库提交备份文件、恢复数据、环境变量、凭证、真实账号或完整本地路径。
|
||||
- 不发送企微消息、不创建企微审批模板、不把邮箱改为可空、不写占位假数据,也不将敏感授权变成普通登录的强制步骤。
|
||||
- 不导入通讯录中的其余四名成员,不授予 `finance_mgr`,不修改审批规则来放行 CEO 本人无主管报销。
|
||||
- 不在本任务完成企微调休、导航、报销或模块退役;它们属于后续批次。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- `bash -n scripts/backup-db.sh scripts/backup-storage.sh scripts/restore-db.sh scripts/restore-storage.sh <新增脚本>`
|
||||
- 使用临时目录和一次性本地 PostgreSQL 目标运行合成数据恢复演练;若本机缺少 PostgreSQL/Docker,则记录为环境阻塞,不改用真实库。
|
||||
- 覆盖损坏 dump/tar、危险 tar 路径、非空目标目录、缺少确认和确认不匹配等负向场景。
|
||||
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-26-internal-ops-recovery-baseline`
|
||||
- `node scripts/qa/check-trellis-governance.mjs`
|
||||
- `node scripts/qa/check-sensitive-content.mjs`
|
||||
- `git diff --check`
|
||||
- `cd server && node --check src/services/integrations/wecomHrMaster.js`
|
||||
- `cd server && npm test -- --run tests/unit/wecomHrMaster.service.test.js`
|
||||
- `cd server && npm run prisma:validate`
|
||||
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js`
|
||||
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/install-warch3-ops-timer.sh`
|
||||
- `node deploy/trialrun/validate-env-contract.js`
|
||||
- 获批部署窗口内执行迁移状态、Compose 数据挂载、`/ready`、前端 marker、Caddy 公网路由和回滚容器只读复验。
|
||||
|
||||
## Key Decision
|
||||
|
||||
先改进可恢复性和防误操作,再接触真实环境。默认工作流只支持影子恢复;真实恢复不是普通脚本路径,而是需要停机、负责人确认、成对备份、回滚点和独立授权的事故操作。
|
||||
|
||||
企微敏感字段采用“日常静默登录 + 一次性本人授权开通”的双入口。批量同步不猜测敏感资料;手机号按已批准范围改为全局 nullable,空值统一保存为 `NULL`。自助开通只授予 `employee`,业务角色继续由管理员审计写入。
|
||||
|
||||
## Local Evidence
|
||||
|
||||
- 已使用一次性 PostgreSQL 16 容器和合成附件完成多轮往返演练,最近一次本地参考 RTO 为 6 秒。
|
||||
- 演练覆盖 manifest/校验和、错误确认、正式风格库名、损坏 DB、损坏 tar、非空附件目录、默认试运行附件目录和危险 tar 路径拒绝。
|
||||
- 该证据不包含真实业务数据、真实运行实例、异机副本或部署后 `/ready`,因此最后一项验收保持未完成。
|
||||
- 2026-07-27 完成手机号 nullable 本地切片:Prisma 仅新增 `phone DROP NOT NULL` migration,用户创建/更新/批量导入、HR master、本人授权和前端空值合同已同步;本轮没有执行 migration 或写入真实员工。
|
||||
- 合成通讯录覆盖说明行/中文表头、5 个手机号加 1 个缺失、非法手机号、缺企业邮箱、重复账号/非空手机号/企业邮箱、双重 apply 门和脱敏报告。管理员提供的真实 XLSX 仅在内存中只读解析为 6 名 active、5 个手机号、1 个缺失、6 个企业邮箱、0 个不完整;未连接数据库,文件保持 Git 排除。
|
||||
- 最终本地门禁:后端全量 163 个测试文件、1016 个测试通过;补充双 apply 门测试后,通讯录导入聚焦 8 项再次通过。Prisma validate、前端 typecheck/build、Trellis task/governance、敏感扫描、流程合同、文档注册审计和 `git diff --check` 通过。文档审计保留仓库既有 22 个未注册文件 warning,无新增 error。
|
||||
|
||||
## Live Read-Only Evidence
|
||||
|
||||
- 获批只读核查结果见 `research/live-read-only-baseline.md`,结论为 `NO_GO_FOR_FORMAL_INTERNAL_USE`。
|
||||
- 公开 HTTPS、TLS、HSTS 和 `/ready` 当前可用,但运行环境没有后端/前端构建标识,且远端迁移少于本地当前基线。
|
||||
- 真实角色只有一个活跃账号,财务和付款复核候选均为 0,不满足 maker-checker。
|
||||
- 只读核查时的历史 DB/附件备份约 55 天未更新、无 manifest,且与运行数据位于同一设备;后续异机批次虽通过传输校验,但复核发现混用了非活动附件目录,已降级为无效恢复证据,须另行批准后从活动 DB/附件重新生成。
|
||||
- 当前代码已在获批后以普通快进方式发布到 upstream,来源门禁为 `PASS`;尚未执行角色修改、部署、迁移或真实格式影子恢复,因此最后一项验收继续保持未勾选。
|
||||
- 当前 OA 是宿主机 Caddy、容器化 server/frontend、独立 PostgreSQL 容器和宿主附件挂载组成的混合部署;公网 OA 路由确实进入 server 容器。
|
||||
- 服务器存在两套无 Git 元数据的部署目录:运行容器来源与日常运维任务指向不同目录,Compose 和环境文件也不一致;运行代码与本机当前代码不一致且缺少构建标识。
|
||||
- 现场端口与历史部署脚本的固定健康检查端口不一致;本地脚本现已动态解析,但未统一活动数据目录、项目名、端口和环境文件前仍不得部署。
|
||||
- 企微结构化探测证明 6 名成员的批量详情接口均成功并返回部门,但手机号、邮箱和企业邮箱可读数量均为 0;HR 接口均成功但没有 `staff_info`。官方文档确认这不代表字段不可获取:2022-06-20 后新建自建应用应由管理员在应用详情选择敏感字段,再由成员通过 `snsapi_privateinfo` 手工授权,并用 `user_ticket` 调 `/cgi-bin/auth/getuserdetail` 获取本人手机号、邮箱或企业邮箱。
|
||||
- 当前 OA OAuth 只使用 `snsapi_base`,且没有调用 `/cgi-bin/auth/getuserdetail`,因此现有登录链路不能采集敏感字段。两名目标各唯一匹配且身份不同,但批量 dry-run 均缺 `phone`、`email`,真实写入已按门禁停止。
|
||||
- 本轮已在本地实现默认关闭的一次性 `onboard` 入口;普通登录继续静默。全量后端门禁与安全补修后的企微聚焦回归结果记录在 `implement.md`。该证据不代表功能已部署,也不代表企微后台敏感字段已由管理员选择。
|
||||
- 2026-07-27 经明确授权,仅把管理员通讯录的两人内存子集通过 stdin 送入活动 server 做生产 dry-run:输入 2、active 2、手机号存在 2、企业邮箱存在 2、不完整 0,计划创建 2、更新 0、冲突 0、跳过 0、失败 0;未执行 apply、未写数据库、未持久化筛选文件,其余四人未进入输入。
|
||||
- 首次正式 CLI dry-run 暴露生产 Node 对 `fs.promises.readFile(0)` 不兼容并在解析前停止;随后用同一解析器、同一 HR master 服务和同一脱敏报告构造器,仅将 stdin 读取替换为 fd 兼容方式完成上述 dry-run。本地修复与回归已随 `4886f9b` 部署,部署后正式 CLI 复验得到相同脱敏结果;本阶段仍未授权 apply。
|
||||
- 2026-07-27 经再次明确授权执行两人子集 apply:创建 2、成功 2、冲突/跳过/失败 0;写后确认 2 名 active 员工、2 个不同企微绑定且初始角色均严格为 `employee`。随后通过现有角色接口写入 `employee + finance` 与 `employee + ceo`,token version 合计递增 2、活动会话保持 0、角色审计新增 2、CEO 待办 0→0、付款批次数量变化 0。
|
||||
- 最终生产只读核对确认两人角色与权限矩阵一致,`finance` 包含已接受的薪资/银行资料权限且不含 `asset.transfer`,`ceo` 包含付款复核且不含 `asset.transfer`;本地合成权限和付款批次 maker-checker 回归 40 项通过,未在生产创建测试付款数据。目标外四人未进入本轮 stdin,未由本轮创建账号。
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# 真实内部实例只读基线
|
||||
|
||||
## 边界
|
||||
|
||||
- 核查日期:2026-07-26。
|
||||
- 已获得只读核查授权。
|
||||
- 未修改远程文件、环境变量、数据库、角色、容器、反向代理或企微配置;未执行备份、恢复、部署和业务写入。
|
||||
- 证据只记录布尔值、数量、时间和短构建标识状态,不保存主机名、IP、SSH 别名、Secret、连接串、账号标识或业务正文。
|
||||
|
||||
## 公开入口
|
||||
|
||||
| 检查 | 结果 | 判断 |
|
||||
| --- | --- | --- |
|
||||
| HTTPS 首页 | `200` | 可访问 |
|
||||
| TLS 证书 | 存在,截止 2026-10-19 | 当前有效,需纳入到期提醒 |
|
||||
| HSTS | 存在 | 通过 |
|
||||
| CSP | 未发现 | 非本批 P0,但公网继续使用时列入后续安全任务 |
|
||||
| `/ready` | `data.ok=true` | 当前进程与基础依赖可用 |
|
||||
| `/ready` 构建标识 | 缺失 | 无法由公开接口追溯运行 commit |
|
||||
|
||||
## 运行来源与迁移
|
||||
|
||||
| 检查 | 结果 | 判断 |
|
||||
| --- | --- | --- |
|
||||
| 容器 | server/db 正在运行,容器内 `/ready=true` | 运行中 |
|
||||
| 运行端口 | 仍使用历史宿主端口基线 | 与当前 Compose 基线不一致 |
|
||||
| `BUILD_SHA` | 容器未配置 | 无法证明运行来源 |
|
||||
| 前端 build marker | 缺失 | 无法从静态资源证明运行来源 |
|
||||
| Prisma migration | 远端已应用 58,本地当前 61;最新项不匹配 | 远端落后,禁止直接宣称当前版本已部署 |
|
||||
| 来源门禁复验 | `PASS`,当前提交已发布 | 已完成普通快进发布,范围 `325a795..7658051`,未使用覆盖参数 |
|
||||
|
||||
## 角色和付款职责
|
||||
|
||||
只读聚合结果:
|
||||
|
||||
- 活跃员工账号:1。
|
||||
- 活跃角色只有 `admin` 和 `employee`,属于同一账号的组合。
|
||||
- 财务处理角色数量:0。
|
||||
- `manager`/`ceo` 付款复核候选数量:0。
|
||||
- 可形成不同自然人的制单/复核组合:否。
|
||||
- 远端旧 schema 尚无当前 `system_admin` 角色值。
|
||||
|
||||
结论:当前真实角色配置不满足付款 maker-checker,也不满足“系统管理员与业务审批职责分开”的当前规约。不得启用真实付款闭环。
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
| 检查 | 结果 | 判断 |
|
||||
| --- | --- | --- |
|
||||
| DB dump | 7 份 | 有历史产物 |
|
||||
| 附件 tar | 7 份 | 数量上可配对,但无 manifest 证明批次关系 |
|
||||
| manifest | 0 | 不符合新成对备份合同 |
|
||||
| 最新产物年龄 | 约 1316 小时(约 55 天) | 明显过期 |
|
||||
| 备份介质 | 与 DB 数据和附件目录位于同一设备 | 不构成异机/异盘副本 |
|
||||
| 目录权限 | `755` | 备份目录可读范围偏宽,后续应按运维账号收紧 |
|
||||
| 真实恢复证据 | 无 | 本地 6 秒合成演练不能替代真实格式影子恢复和 `/ready` |
|
||||
|
||||
结论:当前历史备份不能证明满足 RPO 24 小时、异机副本或可恢复性,不得作为正式启用依据。
|
||||
|
||||
## Go / No-Go
|
||||
|
||||
结论:`NO_GO_FOR_FORMAL_INTERNAL_USE`。
|
||||
|
||||
服务当前可访问不等于运行与恢复底线通过。进入下一步前必须按顺序完成:
|
||||
|
||||
1. 已将当前获批代码以普通快进方式发布到 upstream,来源门禁复验为 `PASS`。
|
||||
2. 重新从活动 DB 与活动 server 附件挂载源生成 DB + 附件 + manifest,并复制到独立主机;此前批次混用了非活动附件目录,不能作为活动实例的成对恢复证据。
|
||||
3. 明确至少一名财务处理人和另一名 `manager`/`ceo` 付款复核人;系统管理职责单独分配,应急 `admin` 独立托管。
|
||||
4. 通过获批部署任务升级代码和 3 项迁移,验证 `BUILD_SHA`、前端 marker、`/ready`、权限矩阵和回滚点。
|
||||
5. 在独立影子目标恢复同一批次 DB/附件,验证应用 `/ready` 和关键附件引用,记录真实 RPO/RTO。
|
||||
|
||||
发布和远程备份已分别获得授权并完成;真实角色写入、部署和影子恢复仍须继续取得明确授权,不能由既有批准推导。
|
||||
|
||||
## 后续获批写入:已降级的异机备份批次
|
||||
|
||||
- 执行日期:2026-07-26。
|
||||
- 授权范围:生成真实 DB、附件和 manifest 成对备份,并复制到用户指定的另一台主机。
|
||||
- 源机与目标机设备指纹不同;未记录主机名、地址、SSH 名称、账号或完整目录。
|
||||
- 批次 `hyb-oa-backup-20260726T133923Z-live` 的三份产物及传输校验本身完整,但后续拓扑复核证明其来源是“活动 DB + 非活动附件目录”。该批次保留作审计,不再计为活动实例的成对恢复点。
|
||||
- 该批次的附件归档含 54 个文件,而活动 server 实际附件挂载为空,活动库附件类记录也均为 0;两端哈希一致只能证明传输一致,不能证明源选择正确。
|
||||
- 目标批次目录权限为 `700`;传输失败产生的单文件残缺副本已精确删除,本机临时暂存数据已安全清理。
|
||||
- 运行主机没有宿主 Node.js,因此为成对备份入口增加 Python 3 manifest 等价回退;无 Node 路径已通过完整合成恢复演练,本地参考 RTO 仍为 6 秒。
|
||||
- 运行中的 PostgreSQL 容器参数与部署目录环境文件存在漂移;本次只在运行容器参数与活动服务数据库连接相互匹配后执行备份,未修改环境文件。
|
||||
- 必须在另行批准后,显式提供经挂载核验的 `STORAGE_DIR_HOST` 和 Compose 解析出的 DB 容器 ID,重新生成并复制正确批次。本次未执行恢复、部署、迁移、角色修改、企微写入或业务写入;真实格式影子恢复和恢复后 `/ready` 仍无证据。
|
||||
|
||||
## 后续获批调查:部署拓扑与企微字段可用性
|
||||
|
||||
- 当前 OA 公网路由由宿主反向代理进入 server 容器;后端和前端构建位于同一应用镜像,数据库位于独立容器,附件由宿主目录挂载。
|
||||
- 运行容器来源目录与日常运维任务目录不同;两套目录都没有 Git 元数据,源文件相同但 Compose 与环境文件摘要不同。运行代码与本机当前代码不一致,`/ready` 无构建标识。
|
||||
- 核查时仓库部署脚本的固定宿主健康检查端口与现场实际端口不一致;本地现已改为 Compose 动态解析,并移除固定容器名、参数化数据目录,但新项目名和数据路径尚未在获批停机窗口启用,故仍未执行部署、重启、迁移或目录改写。
|
||||
- 使用修正后的双 Secret 读取链路做结构化探测:成员详情 6/6 成功、部门 6/6 可读,手机号/邮箱/企业邮箱均 0/6 可读;HR 详情 6/6 返回成功但 `staff_info` 为 0/6。该结果只证明批量读取链路拿不到敏感字段。
|
||||
- 企微官方文档复核:2022-06-20 后新建的自建应用不再通过 `/user/get` 返回手机、邮箱和企业邮箱;管理员需在应用详情选择敏感字段,成员使用 `snsapi_privateinfo` 手工授权,`/cgi-bin/auth/getuserinfo` 才返回 30 分钟有效的 `user_ticket`,随后 `/cgi-bin/auth/getuserdetail` 可返回成员同意的手机号、邮箱和企业邮箱。
|
||||
- 当前 OA `server/src/routes/wecomAuth.js` 固定使用 `snsapi_base`,只调用 `getuserinfo` 获取 userid,不调用 `getuserdetail`;因此现状是项目尚未接入官方敏感信息授权链路,而不是企微没有该能力。
|
||||
- 两名目标成员的脱敏结果为匹配计数 `[1,1]`、身份不同、均 active、数据库匹配 `[0,0]`、计划动作均为 create;两人均缺 `phone` 与 `email`,因此没有员工、角色、会话、付款或其他业务写入。
|
||||
|
||||
## 2026-07-27 复核后的执行授权边界
|
||||
|
||||
- 外部复核确认提交 `2334203` 的 Compose、端口、容器名、备份来源硬失败和负向契约方向正确,同时发现正式 deploy 仍会创建默认目录、相对默认值缺少生产护栏、非 Git 主机 manifest 无来源标识三项本地缺口。
|
||||
- 已批准在补齐上述缺口后执行:发布批准提交、生产只读冻结、从活动 DB/附件重做异机成对备份、在隔离影子目标恢复并验收。该授权不包含停机、生产数据目录复制、迁移、新 Compose 启动、Caddy/timer 修改、企微写入或角色写入。
|
||||
- 冻结证据必须包含实测旧 Compose project、不可变镜像 ID/digest、成功 migration 数和固定关键表计数;影子恢复必须逐表比对计数、比对附件普通文件数并运行 schema 兼容镜像 `/ready`。
|
||||
- 当前旧实例没有 build SHA,重做备份不得写入待部署提交。manifest 记录活动容器的实测 `sha256:` 镜像 ID;新版本部署后的备份才能记录其真实 build SHA。
|
||||
- 后续停机预案采用“旧 PostgreSQL 完全停止后复制到空新目录,旧目录原位保留且不再写入”,不把原子改名后的唯一数据副本当作回滚点。
|
||||
|
||||
## 2026-07-27 正确异机批次
|
||||
|
||||
- 已从活动 Compose project 的唯一 DB 容器和活动 server 的真实附件 bind mount 生成正确源端批次 `hyb-oa-backup-20260726T224546Z-live-corrected`;没有读取漂移的部署 `.env` 来猜来源。
|
||||
- manifest 状态为 `complete`,来源只记录活动 server 镜像 digest,`buildSha` 保持空值;DB catalog、两份产物 SHA-256、字节数和附件 tar 可读性均已复验。
|
||||
- 活动附件目录与归档解出的普通文件数均为 0,与冻结基线及附件相关业务表计数一致;批次目录权限为 `700`。
|
||||
- 本轮临时上传的三个备份脚本已精确删除;旧错误批次和历史备份均未删除。
|
||||
- 已核对备份机与部署服务器是两个不同目标,补齐本机备份机 SSH 别名,并使用既有密钥认证;未把备份写入部署服务器,也未把备份机当作 OA 部署目标。
|
||||
- 正确批次已复制到 ARM 备份机;目标端重新计算 manifest、DB dump 和附件归档 SHA-256,三者均与源端一致,manifest 合同、0 个附件普通文件和目录权限复验通过。旧错误批次继续保留作审计。
|
||||
- 已使用正确异机批次在不发布端口的一次性 PostgreSQL 16 容器中完成真实备份格式影子恢复;58 项成功迁移、12 张关键表计数与冻结基线逐项一致,附件归档安全检查和解包后的 0 个普通文件也一致,包含镜像拉取的实测 RTO 为 33 秒。
|
||||
- 恢复演练后,一次性数据库容器、临时附件目录和本轮新增 PostgreSQL 镜像均已删除;ARM 继续只承担备份与恢复演练职责,没有运行 OA server 或开放 OA 端口。
|
||||
- 当前证据证明 DB+附件异机恢复点可用,但不替代部署环境的应用 `/ready`、构建标识、Caddy 和前端 marker 验收。生产服务未停止,数据库、附件、容器、Caddy、timer、角色和企微均未修改。
|
||||
|
||||
## 2026-07-27 获批生产切换与运维收口
|
||||
|
||||
- 已完成获批生产切换:新 Compose project 成为唯一活动来源,旧项目容器保持停止;旧数据目录和旧容器均保留,正式活动数据使用停机后复制的新目录。
|
||||
- 生产共 61 项 migration 成功,12 张关键表计数与冻结基线一致,活动附件普通文件数为 0;server 只绑定回环地址,DB 未发布宿主端口。
|
||||
- 容器内与公网 `/ready`、前端 marker 均观察到批准提交的同一 12 位构建标识;runtime、外部依赖和公网资源标识检查通过。
|
||||
- 运维 timer 已重新安装并显式绑定当前 Compose project、活动 DB/附件目录和前后端构建标识;timer 为 enabled/active,daily service 保持 inactive,未手工触发真实告警。使用同一组脱敏环境事实执行 Compose config 与 server exec 均通过。
|
||||
- timer 安装前的路径与构建门禁先在本地通过;一次旧正则误拒绝绝对路径后,安装在 unit 写入前停止,随后修复并增加防回退契约。全程未输出或写入 Secret、完整宿主路径和环境原文。
|
||||
- 部署与恢复基线现已完成;真实职责分配、两名成员本人敏感资料授权和企微开通仍是独立未完成门禁,因此整体仍不得宣称付款 maker-checker 或企微自助开通已可用。
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "internal-ops-recovery-baseline",
|
||||
"name": "internal-ops-recovery-baseline",
|
||||
"title": "内部OA运行与恢复底线",
|
||||
"description": "为5至10人内部OA建立低维护、可验证、防误操作的数据库与附件成对备份恢复基线,并明确真实环境授权门。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "backup-restore-safety-local-drill-operational-baseline",
|
||||
"package": null,
|
||||
"priority": "P0",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-07-26",
|
||||
"completedAt": "2026-07-27",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"scripts/backup-db.sh",
|
||||
"scripts/backup-storage.sh",
|
||||
"scripts/backup-all.sh",
|
||||
"scripts/restore-db.sh",
|
||||
"scripts/restore-storage.sh",
|
||||
"scripts/qa/run-backup-restore-drill.sh",
|
||||
"deploy/trialrun/runbook.md",
|
||||
"deploy/trialrun/checklist.md",
|
||||
"server/README.md",
|
||||
".trellis/spec/hyb-oa/deployment-trialrun.md"
|
||||
],
|
||||
"notes": "本地安全基线已普通快进发布,来源门禁复验 PASS;获批真实成对异机备份已完成且两端校验一致。整体仍为 NO_GO:运行实例缺少构建标识、迁移落后、无财务/复核职责分离,角色写入、部署/迁移和真实格式影子恢复仍需另行授权。",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户