Skip to content

数据范围:配置与审计对象按主体过滤,配置类菜单按岗位裁剪(#30) - #32

Merged
baozhoutao merged 3 commits into
mainfrom
issue-30-data-scope
Sep 4, 2026
Merged

数据范围:配置与审计对象按主体过滤,配置类菜单按岗位裁剪(#30)#32
baozhoutao merged 3 commits into
mainfrom
issue-30-data-scope

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

关联工作项 #30(数据范围:配置 / 审计对象按主体过滤,菜单按岗位裁剪)。

为什么这一改的地基是 OWD,不是权限集

工作项原本的方向是「权限集从 readOrg 收成 own + 共享」。照做不会生效,原因在平台侧:

// plugin-sharing 17.2.0 — effectiveSharingModel()
if (m === 'private')            return 'private';
if (m === 'public_read')        return 'read';
if (m === 'public_read_write' ||
    m === 'controlled_by_parent') return 'public';

buildReadFilter() 的第一道闸门是 if (effectiveSharingModel(schema) !== 'private') return null; —— 返回 null = 不加任何过滤。指标下达 / 参与主体 / 到人分工的 OWD 是 controlled_by_parent(在共享层被当成 public),审核记录 / 归档快照是 public_read,读侧过滤在这两种 OWD 下压根不参与求值,权限集里的 readScope 也随之失效。

所以先把这五个对象的 sharingModel 收成 private,记录级共享与 readScope: 'own' 才开始起作用。

改了什么

  1. 对象 OWD —— kpi_plan_subject / kpi_plan_indicator / kpi_staff_assignmentcontrolled_by_parentkpi_review_record / kpi_snapshotpublic_read,一律收成 private
  2. 动态共享规则(services/sharing-service.ts)—— 沿用既有的「按方案建规则、发布时对账、方案关闭后降只读」同一套机制,不新造轮子:
    • 每个参与主体新增参与主体 / 指标下达 / 到人分工 / 归档快照四类只读规则,分管领导各配一条同名 _leader_ 变体;
    • 审核记录与「本单填报单的核对任务」按填报单建规则 —— 审核记录上没有主体列,而平台共享条件只认对象自己的列、不支持跨对象遍历;一张填报单只属于一个主体,粒度比按主体更细。
    • 「本单核对任务」这条只给总公司部门主体:分公司既是被考核主体、又是核对方,它自己那张填报单上挂着全部分公司的核对任务 —— 给了它这条规则,华东的核对人员就会在自己那张单上看见华南、华北的核对意见,与「分公司核对人员只开放本分公司相关行」的拍板口径直接冲突,取窄的那条。(这是自测中 software-flow.mjs T17c 抓到的真缺陷,已修并补单测。)
  3. 权限集(security/index.ts)—— 新增 readOwn;部门填报 / 分公司核对 / 分管领导三套在上述对象上由 readOrg 改用它,可见性完全交给记录共享。人力审核 / 人力负责人 / 管理员保持 org 全量。指标库作为公共口径不动。
  4. 补物化 hook(services/scope-materialize.ts,新文件,在 objectstack.config.ts 注册)—— 审核记录与归档快照由 hook 以系统上下文写入,平台对 isSystem 写入跳过共享物化,官方补偿手段是重新求值规则。与 hooks/sheet.hook.ts 里既有的 kpi_check_task / kpi_result 补偿同路,只是触发点换成记录自己的 afterInsert;重申时按填报单收窄,避免「插一条审核记录 → 重申全方案所有单的规则」的平方级写放大。src/hooks/ 下既有文件零改动(该目录有并行工作项在改)。
  5. 导航裁剪(apps/index.ts)—— 用 requiredPermissions 而不是 visible。实测本版本控制台对导航项的 visible 根本不求值(谓词原样下发到浏览器后被忽略,分组与叶子项都不生效),已按「只上报不修复」报到平台:App nav item visible (CEL) is served to the client but never evaluated — a silently inert gate (17.2.0) objectstack#15135requiredPermissions 是服务端闸门,不满足的项在 /meta 里就被剥掉。
  6. 平台管理员 ↔ 考核系统管理员权限集绑定(security/bind-admin-set.ts,新文件)—— requiredPermissions 逐条 AND、精确匹配、无超级用户豁免,不加这条绑定连管理员自己都会丢掉两个菜单组。按「持有 admin_full_access 的用户」匹配而不是按邮箱,换人 / 多管理员 / 改邮箱都不需要改代码。

验证

  • pnpm verify 绿:validate / typecheck / 103 单测(新增 12 条)/ i18n 同步(本次未新增任何 label,zh-CN 529 键无变化)。
  • 软件档案 scripts/software-flow.mjs:54 / 54 全 PASS(空库重建)。
  • 默认档案 scripts/e2e-flow.mjs:72 PASS / 1 FAIL。失败项 T39 是一条「共享规则条数」的精确常量断言(EXPECTED_RULES = 29,实测 56),新增规则类别后该常量按定义必须同步;scripts/ 在本次派发的禁改清单内,故未改 —— 已按失败升级在工作项上给出逐项核对到条的新常量与改法,交调度员裁决。所有数据范围类断言(T50 / T51 / T53 / T55~T57 / T64 / T70)全 PASS。
  • 六个账号 × 八个对象逐一计数、越权 URL 与 REST 一律 404、导航裁剪的 /meta 下发内容,证据与截图见工作项评论。

测试报告:#30 (comment)
需求符合度清单:#30 (comment)
截图:acceptance-evidence 分支 issue-30/,commit 224e77a0611679ecea92d9f264ae12506b182e4b

🤖 Generated with Claude Code

@baozhoutao

Copy link
Copy Markdown
Contributor Author

代码评审报告(os-project-dev-review)

档位:全量档(依据:改已有共享规则与权限集 = 高风险;diff 10 文件 +518/−31;含五个对象 OWD 收成 private 与两处范围外新增)
结论:可合并

评审为冷启动独立只读:输入只取工作项 #30 全文、PR diff、需求符合度清单原文;未读开发方测试报告、开工前方案评论、交接评论或对话记录。全部结论以自建实例实测为准。

实测环境(自建、自有端口、空库):临时 clone issue-30-data-scope @ 1eb5051(基线 a760651)。软件档案实例 :3117 + .objectstack/review-32.db;默认档案实例 :3116 + .objectstack/e2e-default.db(换档案前已 rm -rf dist .objectstack 重建)。两个实例均经 lsof 核对监听进程 cwd 为本评审的 clone 目录,停进程按 PID,未使用 pkill。未进入主仓库或任何 kpi-issue-* / kpi-software-demo 目录。


通用质量(/code-review 高档)

无 🔴 / 🟡 级发现。pnpm verify 全绿:validate(17 对象 184 字段)、typecheckvitest 103 通过、i18n:extract:check 529 词条同步。diff 内无 TODO / FIXME / @ts-ignore / eslint-disable / 被注释掉的校验 / skip( / only(

两处新增失败路径的错误处理是显式且有意的:scope-materialize.tsreassertbind-admin-set.tsrun 都只记日志、不抛出,理由(数据范围可补偿,不该阻断业务动作 / 引导启动)在代码注释里写明,与既有 sheet.hook.ts / plan.hook.ts 的同类处理一致。provisionPlanSharing 的对账只在 options.reconcile 为真时执行,补物化走的窄重申路径不传该项 —— 已核实不会误停用其他规则。


专属核查

1 改动面越界:✅ 无越界

  • 10 个文件逐个对照:6 个在工作项范围 §1/§2/§3 内(sharing-service.tssecurity/index.tsapps/index.tstest/sharing.test.ts,及 OWD 所在的三个 *.object.ts 中的两个);objectstack.config.tsbind-admin-set.tsscope-materialize.ts 三处已在符合度清单末尾「范围外但需要知会的两项改动」明确披露,非静默扩围。
  • 五个对象的 OWD(kpi_plan_subject / kpi_plan_indicator / kpi_staff_assignment / kpi_review_record / kpi_snapshot)收成 private:工作项 §1 要求「权限集从 readOrg 收成 own + 共享」,而平台 plugin-sharingbuildReadFilter 只在 effectiveSharingModel(schema) === 'private' 时才建过滤,controlled_by_parentpublic_read 分别被归为 public / read 直接 return null —— 不改 OWD,readScope: 'own' 就是死条款。已读平台源码逐行核实,该改动是 §1 的必要前置,不是自选动作。
  • 禁触碰面 diff 全空:src/hooks/src/actions/index.tssrc/data/scripts/docs/、README、CLAUDE.md 均为 0 改动(git diff --stat 逐项核对)。
  • src/apps/index.ts:把 diff 中每行剥掉 , requiredPermissions: PLAN_CONFIG_MENU 后做配对比较,残留的未配对行只有新增注释块、importconst PLAN_CONFIG_MENU 三处纯新增 —— 导航项的 id / label / icon / objectName / dashboardName 与分组结构一字未动。✅ 只改可见性,未改结构与文案。

2 降级对账:✅ 清单相符

在自建空库实例上复现清单的核心计数,逐条对上(REST limit=500 实测):

账号 指标下达 参与主体 到人分工 审核记录 归档快照 核对任务 指标库
管理员 / 马丽(人力审核)/ 何平(人力负责人) 36 11 8 94 1 30 24
张伟(研发部·部门填报) 3 1 1 8 0 3 24
赵敏(销售部·部门填报) 3 1 1 16 1 3 24
陈东(华东·分公司核对) 4 1 0 7 0 10 24
徐涛(技术线·分管领导) 9 3 3 24 0 9 24

与清单第 1–10、24–29 条一致(清单第 4 条写「全量 94」,与实测同)。指标库对六类岗位一律 24 条 —— kpi_indicator 的 OWD 与授权确未被动过,清单第 10 / 22 条成立。

  • 同口径核实:六个对象的规则与既有填报单规则用同一族收件方 —— 主体维度 recipientType: 'unit_and_subordinates' + recipientId: 主体单元,分管维度 recipientType: 'user' + recipientId: leader,同一 planRulePrefix 命名空间、同一发布期对账、同一「关闭/归档后降级」处理(这六类本就恒为 read,不需降级,新增单测对此有断言)。✅
  • 新增单测非空断言:12 条新用例均为实断言 —— toMatchObject 带具体 object / criteria / recipientType / recipientId / accessLevel;含反向断言(scheck_sheet_east 必须不存在、华东无分管领导则不产生领导规则);含不变量断言(最坏形态规则名长度恰为 99 ≤ RULE_NAME_MAX)。若 byName.get() 落空,toMatchObject 直接失败,不会静默通过。✅

3 三禁痕迹:✅ 无

  • diff 路径不含 node_modules / 依赖目录,无 patch 类文件。
  • scope-materialize.ts 不是绕行:它不直写 sys_record_share(平台明示该表引擎独占、数据 API create 返回 405),而是调用本应用既有的 provisionPlanSharing 重新声明 sys_sharing_rule —— 这正是平台日志给出的官方补偿手段(sharing materialisation skipped for isSystem writes; re-evaluate rules or restart to backfill)。系统上下文取自 hooks/util.tssys(ctx) → 平台 IScopedContext.sudo(),与既有全部 hook 同一通道,不是伪装用户上下文
  • bind-admin-set.ts 同样走标准面:写 sys_user_permission_set(平台标准授权表),挂 kernel:bootstrapped,与既有 bind-position-sets.ts 同构;先查后插,幂等。
  • 导航闸门用的 requiredPermissions(导航项)与 systemPermissions(权限集)都是 spec 一级键,且 app.zod.ts 明确「navigation ITEM 的 requiredPermissions 服务端强制剥离」。属平台标准机制的正常使用。

4 硬拍板落地:✅

  • 本单不新增任何数字字段(diff 中无 Field.number / currency / percent),四件套不适用。
  • 新增用户可见文案只有动态共享规则的 label / description(如「参与主体共享给研发部及其下级」「研发部审核记录共享给分管领导」「考核主体成员」「分管领导」):名词取自需求文档术语表(参与主体 / 指标下达 / 到人分工 / 归档快照 / 审核记录 / 分管领导),无内部代号、无机器名、无异常原文。✅ 过 std-copy 红线。
  • 导航项与权限集 label 未新增、未改动;新增的两条失败路径只写服务端日志,不向用户抛文案。
  • i18n:extract:check 通过,元数据 label 与 zh-CN 包同步。

调用面抽查(全量档专项)

OWD 收成 private 后,逐条核实既有读路径未被改坏:

# 读路径 结论 依据
1 结果汇总服务 results-service.regenerateResults(读参与主体 / 到人分工 / 个人承接项) ✅ 未受影响 调用点在 sheet.hook.ts / bonus.hook.ts / adjustment.hook.ts,传入的 api 一律是 sys(ctx)(sudo() 系统上下文),不经共享过滤
2 归档快照服务 snapshot-service.createSnapshot(读审核记录、写快照) ✅ 未受影响 同上,createSnapshot(api, …)api 来自 const api = sys(ctx)
3 共享服务 provisionPlanSharing(读方案 / 主体 / 分工 / 填报单) ✅ 未受影响 四个调用点全部传 sys(ctx)
4 hook 内部读(发布完整性校验、方案复制、分公司清单、核对任务生成、争议、填报明细) ✅ 未受影响 src/ 内对这五个对象不存在任何用户上下文读;grep 逐个对象核对,全部经 sys(ctx)
5 人力审核 / 人力负责人 / 管理员的全量可见 ✅ 未被改坏 三者保留 readScope: 'org';平台 buildReadFilter__readScope === 'org'return null,与 OWD 无关。实测三者计数与全量基准逐对象一致(36/11/8/94/1/30)
6 分公司核对人员对填报单的 org 读 ✅ 未被改坏 kpi_entry_sheet 授权本次未动,其 OWD 改前即为 private。实测陈东可见 11 张(全量)
7 kpi_personal_item(现私有的到人分工的主从子记录) ✅ 随主记录收窄,注释属实 实测张伟可见 1 条、取他人记录 404;controlled_by_parent 的下推由 ObjectQL 在上游结算(ADR-0055),与 plugin-sharing 的记录共享层是两层,故未被 effectiveSharingModel → public 短路
8 端到端业务链路 ✅ 未被改坏 自建实例 software-flow.mjs 54/54 全 PASS(含 T15a–e 四维结果、T16a–d 归档快照、T17a–d 数据范围);默认档案 e2e-flow.mjs 72 PASS / 1 FAIL,唯一失败为 T39 常量(见下),T50/T51/T53/T55/T56/T57/T64/T70 数据范围断言全 PASS
9 越权直取 ✅ 一律 404 张伟对指标下达 / 参与主体 / 到人分工 / 个人承接项 / 审核记录 / 归档快照 / 核对任务七个对象取他人记录一律 HTTP 404,取本人记录 200;强制按他部门主体过滤返回空集

bind-admin-set.ts 的匹配条件(误绑 / 漏绑):实测生效 —— 绑定后 admin@objectos.ai 新增一行 kpi_admin_set,管理员导航保持完整 24 项。不会误绑:目标集合是「持有 admin_full_access 的用户」,而 admin_full_access 本就是平台超级权限持有者,授予 KPI 域权限集不构成越权升级;先查后插,幂等,重复启动不产生重复行。存在两个漏绑边界(均未在当前档案触发,见下 ⚪-2)。

requiredPermissions 对六类岗位的导航结果 vs 口径矩阵:逐岗位实测 /meta/app/kpi_app/meta/apps 两个端点,结果一致(服务端剥离,不下发浏览器):

岗位 导航项数 与口径第 11 条 是否一致
管理员 / 人力审核 / 人力负责人 / 分管领导 24(完整) ✅ 口径只要求对另两类隐藏
部门填报人员 / 分公司核对人员 16 ✅ 恰好剥掉「考核方案」「基础设置」两组 + 其下 7 个子项(方案版本 / 指标下达 / 参与主体 / 到人分工 / 指标争议 / 指标库 / 流程进度),与口径列举一字不差

「56」是否与本 PR 的规则生成逻辑一致

一致。 按对象 × 收件方独立推导,并在默认档案实例上实测复核,两者精确吻合:

  • e2e-flow.mjsplan3 配置(5 主体 = 2 部门 + 3 分公司,仅市场部配分管领导,2 条到人分工,发布生成 5 张填报单)直接调用 planSharingIntents,实际生成 62 条规则:原有 29 条 + 按主体的配置四类 4×5=20 + 分管领导同族 4×1=4 + 按填报单的审核记录 5+1=6 + 本单核对任务 2+1=3。
  • T39 的 rulesOf 过滤条件是「criteria_json 里出现方案 id」,而 6 条 kpi_review_record 规则的条件是 { sheet }不含 plan(审核记录上没有方案列,这是代码里写明的取舍),因此被该过滤器漏掉 → 计入 56 条
  • 默认档案实例实跑,T39 输出 rules=56 expected=29 —— 与推导值精确一致。

给调度员的提示:同步该常量时,正确值取决于口径 —— 「T39 那条按 criteria_json 过滤的断言」是 56,「本方案实际生成的规则总数」是 62。另外 T39b(「每条规则的条件里都带本方案 id」)当前是空过:那 6 条只带 sheet 的规则在进入 T39b 之前已被 rulesOf 滤掉,断言从未看见它们。若把 T39 改为按方案实配推导,需一并确认 T39b 的口径(规则的方案归属由规则名前缀保证,对账也按名前缀回收,功能上无缺陷)。


发现清单

无 🔴 阻塞,无 🟡 应修。以下为 ⚪ 记录级,不阻塞合并:

位置 问题 处置出口
⚪-1 src/security/bind-admin-set.ts:1(声明)+ src/apps/index.ts kpi_plan_config 只被 systemPermissions / requiredPermissions 按字符串引用,未经 defineCapability 在 stack capabilities[] 里正式声明。按 ADR-0066 D1,这样的能力是「隐式派生、无标题」的,平台权限管理界面里会显示裸机器名 kpi_plan_config 而无中文标签。pnpm validate 接受,不影响功能 留痕;后续可补一条 defineCapability(带中文 label)消除管理界面上的内部代号
⚪-2 src/security/bind-admin-set.ts:237 匹配面只扫 sys_user_permission_setadmin_full_access直接授权行。若某部署把平台管理员权限经 sys_position_permission_set(本项目正是用这张表绑 KPI 岗位权限集)或其他组/角色路径授予,该管理员漏绑,登录后静默丢失两个配置类菜单组;另 limit: 200 在管理员超过 200 人时静默截断。当前全部档案均为直接授权行,未触发 留痕;补一条直接授权行即可自愈,是否加固交维护者
⚪-3 src/security/bind-admin-set.ts:193 「只增不减」是显式设计,但后果是:撤销 admin_full_access 不会连带撤销 kpi_admin_set,被降权的管理员仍保有全部门目标值与权重的全量可见。与本单「各部门目标值互相保密」的意图方向相反,但不在工作项列举范围内 留痕,提请维护者判断是否另立跟踪项
⚪-4 src/security/index.ts:137,152 分公司核对人员与分管领导本次新拿到 kpi_staff_assignment: readOwn(改前二者无此对象授权),但两个权限集都没有 kpi_personal_item 授权。实测二者读个人承接项返回 HTTP 403 —— 「到人分工」详情页的主从子表对这两个岗位会报 403 留痕;是否补只读授权交维护者(补则须同步确认承接项的保密口径)
⚪-5 src/apps/index.ts:46(nav_disputes)+ src/security/index.ts:96 口径第 11 条 允许「隐藏收成只读入口」,实现取了「隐藏」。副作用:部门填报人员的权限集仍声明 kpi_dispute.allowCreate: true,但「指标争议」是该对象唯一的 UI 入口,隐藏后该新建能力在界面上不可达(REST 仍可创建)。属维护者已明确授权的方向,只是声明与入口不再自洽 留痕,提请维护者确认是保留声明、还是改为只读入口
⚪-6 src/services/sharing-service.ts:605 kpi_entry_sheet 取数写死 limit: 2000。方案填报单超过 2000 张时溢出部分静默拿不到审核记录 / 本单核对任务规则(填报单数 = 参与主体数,现实上限远低于 2000) 留痕
⚪-7 src/services/scope-materialize.ts:47 每插入一条审核记录都触发一次 provisionPlanSharing(4 次查询 + 1~2 条规则重申)。已用 sheets 选项把重申收窄到本条记录所属那一张单,避免平方级放大;剩余扇出代价在 sharing-service.ts 既有的「已知取舍」注释里已登记 留痕(既有已知取舍,本单已做收窄)

结论:可合并。 阻塞与应修均为 0;上列 7 条为记录级,按 os-project-dev-review 不阻塞合并。功能是否达标仍归验收线判定,本报告不代替验收。

评审在临时目录进行,评审结束已按 PID 停止两个实例并删除临时目录;未对本仓库任何分支做写操作。

@baozhoutao

Copy link
Copy Markdown
Contributor Author

评审闸门处置 + 合入 main(不切状态、不改处理人)

提交 内容
b0f6cdc 合入 origin/main(到 6c1d02e)—— 无冲突,ort 策略直接合并
f3a03e4 评审三处处置(应修 1 处 + 对账口径 2 处)

修复点 ↔ 提交

# 评审发现 处置 落点(均在 f3a03e4)
1 记录 4(应修):分公司核对人员与分管领导拿到 kpi_staff_assignment: readOwn,却没有 kpi_personal_item 授权,「到人分工」详情页展开主从子表 403 —— 本单引入的用户可见错误 两个岗位补 kpi_personal_item 只读授权,与主记录同范围、不放宽到全量:readScope 写最窄的 own,行的可见性由主记录决定。新增 readOwnChild 常量单点定义并写明「声明它是必需的,不是补充」的理由 src/security/index.ts
1b 同上,补单测 3 条,盯的是不变式而非当前取值:①「声明了到人分工就必须同时声明个人承接项」(三个受限岗位全覆盖)②「新收窄的两个岗位:子表范围 == 主表范围 == own」③「个人承接项一律只读」。部门填报人员的 kpi_personal_item 授权是本次之前就有的(readScope: 'org'),动它属扩围,已在测试里写明为何把它排除在第 ② 条之外 test/sharing.test.ts
2 6 条 kpi_review_record 规则的 criteria_json 只有 {sheet} 不含方案 id,被「含方案 id」的过滤漏掉 → T39 数出 56 而实际生成 62、T39b 空过 rulesOf 改按规则名前缀回收。前缀里的方案片段是稳定散列,脚本不复刻那套算法(复刻就会和实现两地各飘一份),改从「每个参与主体必有一条 <前缀>sheet_<单元> 规则」的锚点反推;锚不到就返回空数组,让精确相等断言当场变红,而不是悄悄退化成「零条也算过」 scripts/e2e-flow.mjs
2b T39 推导式、T39b 断言 T39 推导式扩到新增的对象规则(每主体 8 类、有分管领导的主体 +6、每张单的审核记录、部门主体那张单的核对任务…全部按方案实配推导,不写死)。T39b 改判「条件要么带本方案 id、要么带本方案某张填报单的 id」——两类条数都精确相等且都非空,出现第三类(既不带方案也不带本方案的单)即判红。没有改成下限 scripts/e2e-flow.mjs

一处顺带修好的截断(同文件,说明原因)

改完口径后 T70 变红(岗位规则数出 3 条而不是 4 条)。根因不是本次改动,而是 ?limit=500静默截断:实测 sys_sharing_rule 已有 538 行(一个方案就写近百条规则,几个方案轻松过 500),list() 一把取 500 就丢掉了后面的。截断的症状恰恰是「某条规则凭空消失、断言红在一个与截断毫无关系的地方」,排查成本远高于这几行。故在同一处加了翻页(skip + hasMore),并用注释把这个坑钉住。sys_record_share 实测 377 行,尚未触顶,未动。

两脚本实跑摘要

均为空库重建(rm -rf .objectstack dist 后重启,换档案必须连 dist 一起清 —— 种子档案会烘进 dist),dev 端口 3115(起实例前 lsof -i 确认空闲)。

默认档案 node scripts/e2e-flow.mjs —— 74 / 74 全 PASS

PASS T39  发布按方案配置写入动态共享规则,条数与方案配置精确相符,元数据零改动
          — rules=92 expected=92
PASS T39b 规则条件按方案隔离:要么带本方案 id,要么带本方案填报单的 id(审核记录对象没有方案列)
          — 按方案 82 条 / 按填报单 10 条(期望 10)/ 共 92 条
PASS T50  不存在方案发布之外来源的 KPI 共享规则 — 270 条
PASS T70  方案关闭后人力岗位对该方案填报单降为只读 — 岗位规则 4 条全部 read=true read=200 write=403
{"passed":74,"failed":0}

软件档案 node scripts/software-flow.mjs —— 54 / 54 全 PASS

PASS T17a 部门填报人员只见本部门填报单,打开其他部门的填报单被拒绝 — 可见 1 张:销售部;越权读取 status=404
PASS T17b 到人结果只见本人 — 可见到人结果 1 条,全部为本人:true
PASS T17c 分公司核对人员只见本分公司的核对任务 — 可见 10 条,分公司取值:bu_sw_east
PASS T17d 分管领导只见分管主体的填报单 — 分管 5 个主体,可见 5 张;越权读取 status=404
{"passed":54,"failed":0}

门禁 pnpm verify 绿:validate / typecheck / 110 单测(本轮 +7)/ i18n 同步(未新增 label,zh-CN 529 键无变化)。

个人承接项修复的实测(修复前这三行是 403)

管理员:到人分工 8 条,个人承接项 8 条
张伟(部门填报): 列表 200 / 1 条 | 主从子表 200 / 1 条
陈东(分公司核对): 列表 200 / 0 条(华东无到人分工)
徐涛(分管领导): 列表 200 / 5 条 | 主从子表 200 / 1 条
逐记录:张伟读本部门 200 / 读销售部 404;陈东读研发部 404;徐涛读分管内 200 / 分管外 404

⚠️ 撤回我上一轮报告里的一条「基准差异」

上一轮测试报告的基准差异第 3 条说「controlled_by_parent 的子对象在读侧不随主记录收窄」,是错的 —— 我当时只读了 plugin-sharingeffectiveSharingModel(它把这种 OWD 映射为 public,buildReadFilter 因此不加过滤),就推断读侧不收窄,没有实测。本轮为验证个人承接项的修复做了逐记录实测,结论相反:

对象 管理员可见 张伟(研发部)可见 跨范围逐记录读
kpi_personal_item 个人承接项 8 1 404
kpi_entry_line 填报明细 36 3 404

读侧的收窄由 plugin-security 的主从判定完成,不走共享层的读过滤 —— 两条路径,我上一轮只看了其中一条就下了结论。因此「个人目标值、填报实际值全员可读」这个我上一轮记下的残留风险并不存在,也不需要为它另立工作项。给评审与验收造成的误导由我负责,特此更正。

其余记录级条目(1 kpi_plan_configdefineCapability、2/3 bind-admin-set 漏绑与只增不减、5 争议菜单隐藏但仍 allowCreate、6 limit 2000、7 扇出代价)按轮次纪律留痕不动,未顺手扩围。

提交即止,未合并

@baozhoutao

Copy link
Copy Markdown
Contributor Author

代码评审报告 —— 复查轮(os-project-dev-review)

复查范围:仅修复点 + 是否引入新问题(轮次纪律),不重开全面评审;上一轮留痕的记录级条目一律不动、不升级。
被复查提交:f3a03e4(修复)、b0f6cdc(合入 origin/main6c1d02e)。上一轮评审对象为 1eb5051
结论:可合并

改动面:f3a03e4 只动 3 个文件(scripts/e2e-flow.mjs / src/security/index.ts / test/sharing.test.ts),与三个修复点一一对应,无夹带。scripts/ 本轮由调度员明确放行。

实测环境:独立临时 clone @ f3a03e4。默认档案 :3118 + rc-default.db;软件档案 :3119 + rc-sw.db。两次起实例前 lsof 确认端口空闲,起后核对监听进程 cwd 为本次 clone,换档案前 rm -rf dist .objectstack,停进程按 PID。


e2e-flow.mjs T39/T39b 对账口径 —— ✅ 通过

核查点 结论 证据
确实不是下限 ✅ 精确相等 T39 用 kpiRules.length === EXPECTED_RULES(非 >=);T39b 用 byPlan + bySheet === kpiRules.length && bySheet === EXPECTED_SHEET_SCOPED && byPlan > 0,总数必须被完全归类,出现第三类即红
抓全了、没漏 ✅ 无遗漏 独立比对:rulesOf 返回 92 条,该方案前缀 kpi_pogtwkzeiba7f_s6l03j_ 下的真实全量也是 92 条(active 92)—— 逐条相等,不是子集
锚不到时变红而非空过 ✅ 变红 用伪造方案 id 与对不上的主体两种方式打断锚点,rulesOf 均返回空数组;而 EXPECTED_RULES 恒 ≥ 主体数×8 > 0,=== 当场判红。T39b 另有 kpiRules.length > 0 兜底
推导式按实配、未写死 subs3After.length * 8 + leaderSubs3.length * 6 + 去重领导数 + 2 + 2*2 + sheets3.length + leaderSheets3 + deptSubs3.length + deptLeaderSubs3 —— 每一项都从当次方案实配读出。逐项对照 sharing-service.ts 的生成逻辑(每主体 4 旧 + 4 新配置类 = 8;有领导的主体 2 旧 + 4 新 = 6;每张单审核记录 1 + 领导 1;部门主体那张单核对任务 1 + 领导 1)—— 一致
前缀不复刻散列算法 从「每个参与主体必有一条 <前缀>sheet_<单元> 规则」反推,planRulePrefix = kpi_p<ruleSlug(planId)>__ 收尾,反推出的前缀与实现完全对齐,不会两地各飘一份

实跑(默认档案空库):T39 rules=92 expected=92 PASS;T39b 按方案 82 条 / 按填报单 10 条(期望 10)/ 共 92 条 PASS。上一轮我给出的「56 / 62」两个数字在新口径下已归一为同一个 92(旧口径漏掉的 6 条审核记录规则现已被计入),口径分裂消除。

② 分公司核对人员 / 分管领导补 kpi_personal_item 只读 —— ✅ 通过

核查点 结论 证据(软件档案实测)
随主记录收窄、未放宽 徐涛(分管领导)到人分工 5 / 个人承接项 5(全量 8);陈东(分公司核对)0 / 0;逐行核对子表的 assignment 是否都落在本人可见的主表内 —— 越界 0 行
readScope 是最窄的 own readOwnChild = readOwn,即 readScope: 'own',且 allowCreate/allowEdit/allowDelete 全 false
跨范围仍拒 徐涛、张伟取分管/本部门外的承接项记录一律 HTTP 404,取范围内 200
未顺手扩围 部门填报人员原有的 kpi_personal_item: readScope 'org' 未被改动,实测张伟仍是 1/8(与上一轮同),测试注释写明为何把它排除在同范围断言之外
单测是不变式而非当前取值 3 条:①「声明了到人分工就必须同时声明个人承接项」(三个受限岗位全覆盖,含部门填报人员)②「新收窄的两个岗位:子表 readScope == 主表 readScope == own」③「个人承接项一律只读」。均为跨岗位遍历的结构断言,任何一方将来被放宽都会红,不是快照

上一轮记录的 403(⚪-4)已消除:两个岗位现在拿到的是空表或本范围内的表,不再是报错页面。

sys_sharing_rule 翻页 —— ✅ 通过,未改断言语义

  • 截断属实:实测 sys_sharing_rule 已有 538 行,旧写法 ?limit=500 只取到 500 行。
  • 翻页正确:分页取到 538 条,去重后仍 538(无重复),与单次 limit=5000 取到的 538 条完全一致(无遗漏)——skip 确实生效,不会死循环。
  • 语义未变:rulesOf 的过滤条件一字未改,只是输入从「截断后的 500 条」变成全量;T50 的断言(kpiOwned.length > 0 + every(managed_by !== 'package') + 不含 kpi_share_)原样保留,检查面变大只会更难通过,不构成放宽。

关于「基准差异第 3 条」的撤回 —— ✅ 撤回成立,应当撤回

controlled_by_parent 子对象在读侧确实随主记录收窄,原「全员可读」的残留风险不存在。软件档案逐记录实测:

对象 管理员 张伟(研发部) 徐涛(分管 5 主体) 跨范围逐记录
kpi_personal_item 个人承接项 8 1 5 404
kpi_entry_line 填报明细 36 3 15 404

这与我上一轮的独立实测一致 —— 上一轮报告调用面抽查第 7 项即已记为「随主记录收窄,注释属实」,并未把它列为风险,故本轮无需改判,只是开发方自述的更正。

机制归属补一处更正:收窄由 plugin-security 的主从(master-detail)派生判定完成 —— 其源码明示「a controlled_by_parent detail derives its access from its master」;plugin-sharingeffectiveSharingModel 把该 OWD 归为 publicbuildReadFilter 不加过滤,是另一条路径。开发方本轮的归属描述正确;我上一轮把它记成 ObjectQL/ADR-0055,层级指认有误,结论不受影响,一并更正。

(另更正上一轮报告一处计数笔误:特权岗位完整导航为 25 项而非 24 项,受限岗位 16 项;剥离项数与结论不变。)


是否引入新问题 —— 未发现

  • 生产代码路径本轮只多了两条只读授权,无其他行为改动;security/index.ts 被改后重新逐岗位核对导航,结果与上一轮一致(管理员 / 人力审核 / 人力负责人 / 分管领导 25 项;部门填报人员 / 分公司核对人员 16 项,两个配置类菜单组仍被服务端剥离)。
  • 门禁 pnpm verify 绿:validate(17 对象)/ typecheck / 110 单测 / i18n 529 键同步。
  • 默认档案 e2e-flow.mjs 74 / 74 全 PASS(上一轮为 72 PASS / 1 FAIL,唯一失败的 T39 已修复);软件档案 software-flow.mjs 54 / 54 全 PASS
  • 合入的 b0f6cdc(主线「每个参与主体须配分管领导」)与本单改动无冲突:T39 推导式已按「分管领导按方案实配」推导,合并后两脚本全绿。
  • ⚪(仅留痕,不阻塞、不新增条目):rulesOf 的锚点用 name.endsWith('sheet_' + sub.subject)原始单元 id 比对,而规则名里是 ruleSlug(unit)。当前所有单元 id 均为小写字母数字下划线且不超长,ruleSlug 为恒等,锚点必中;若将来出现需要转义或超长的单元 id,锚点会失配 —— 但失配的表现是返回空数组 → 断言变红,属安全的失败方向,已在代码注释里写明。

结论:可合并。 三个修复点全部核实通过,撤回成立,未发现新问题。上一轮的记录级条目(kpi_plan_configdefineCapabilitybind-admin-set 漏绑与只增不减、争议菜单隐藏但仍 allowCreatelimit 2000、扇出代价)按轮次纪律留痕不动;原记录 ⚪-4(个人承接项 403)本轮已修复消除。功能是否达标仍归验收线判定。

复查在独立临时 clone 进行,两个实例已按 PID 停止、端口经 lsof 确认释放,临时目录已删除;未对本仓库任何分支做写操作。

@baozhoutao
baozhoutao merged commit 01d3360 into main Sep 4, 2026
1 check passed
@baozhoutao
baozhoutao deleted the issue-30-data-scope branch September 4, 2026 03:31
baozhoutao and others added 3 commits September 3, 2026 22:46
各部门目标值与权重互相保密(维护者 2026-09-03 拍板):指标下达、参与主体、
到人分工、审核记录、归档快照、本单核对任务对部门填报人员只开放本部门相关行,
对分公司核对人员只开放本分公司相关行,分管领导只开放分管主体;人力审核 /
人力负责人 / 管理员保持全量;指标库作为公共口径不过滤。

根因不在权限集而在 OWD:平台 plugin-sharing 的 effectiveSharingModel 把
controlled_by_parent 与 public_read 都映射成「读不过滤」,buildReadFilter 在
第一道闸门就 return null,权限集里的 readScope 随之失效。因此五个对象的
sharingModel 先收成 private,记录级共享与 readScope: 'own' 才开始起作用。

- 对象:kpi_plan_subject / kpi_plan_indicator / kpi_staff_assignment 由
  controlled_by_parent、kpi_review_record / kpi_snapshot 由 public_read
  一律收成 private;
- 共享规则:沿用「按方案建规则、发布时对账、方案关闭后降只读」的同一套机制,
  每个参与主体新增参与主体 / 指标下达 / 到人分工 / 归档快照四类只读规则,分管
  领导各配一条;审核记录与「本单核对任务」按填报单建规则 —— 审核记录上没有主体
  列,而平台共享条件只认对象自己的列、不支持跨对象遍历;
- 「本单核对任务」只给总公司部门主体:分公司既是被考核主体又是核对方,给了它
  这条规则,华东的核对人员就会在自己那张填报单上看见华南、华北的核对意见;
- 补物化 hook 单独成文件并在 objectstack.config.ts 注册,不动 hooks/ 下既有
  文件:审核记录与归档快照由 hook 以系统上下文写入,平台跳过共享物化,官方补偿
  手段是重新求值规则;重申时按填报单收窄,避免平方级写放大;
- 导航裁剪用 requiredPermissions(服务端剥离)而不是 visible:实测本版本控制台
  对导航项的 visible 根本不求值,已按只上报不修复报到
  objectstack-ai/objectstack#15135;requiredPermissions 逐条 AND、精确匹配、
  无超级用户豁免,因此把「平台管理员即考核系统管理员」这条隐含安排写成绑定行。

自测:软件档案 software-flow 54/54;默认档案 e2e-flow 72 通过、1 失败(T39 的
共享规则条数常量随新增规则类别机械变化,scripts/ 本轮禁改,已在工作项上报)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
评审闸门三处处置(PR #32):

1. 个人承接项读权(评审记录 4,应修)——分公司核对人员与分管领导上一轮拿到了
   `kpi_staff_assignment: readOwn`,却没有 `kpi_personal_item` 的授权,「到人分工」
   详情页展开主从子表直接 403,是本单引入的用户可见错误。范围与主记录一致、不放宽:
   `readScope` 写最窄的 own,行的可见性由主记录决定。实测三个岗位列表与逐记录读均
   200/404 正确(张伟 1/8、徐涛 5/8、跨范围一律 404)。补了 3 条单测,盯的是「声明了
   到人分工就必须同时声明个人承接项」这条不变式,而不只是当前取值。

2. e2e 共享规则对账口径——按**规则名前缀**回收,不再按「条件里含方案 id」筛:
   审核记录对象上没有方案列(它只认填报单),平台共享条件又不支持跨对象遍历,那批
   规则的条件是 {sheet},含方案 id 的过滤会把它们整批漏掉 —— 上一轮 T39 因此数出 56
   而实际生成 62,T39b 也跟着空过。前缀里的方案片段是稳定散列,脚本不复刻那套算法
   (复刻就会两地各飘一份),改从「每个参与主体必有一条 <前缀>sheet_<单元> 规则」的
   锚点反推;锚不到就返回空数组,让精确相等断言当场变红而不是退化成零条也算过。
   T39 的推导式扩到新增的对象规则,T39b 改判「条件要么带本方案 id、要么带本方案某张
   填报单的 id」,两类条数都精确相等且都非空,不是下限。

3. 共享规则要翻页取全——一个方案现在就写近百条规则,几个方案过 500 上限,一把
   `?limit=500` 会静默截断;症状是某条规则凭空消失、断言红在与截断毫无关系的地方
   (本次是 T70 数出 3 条岗位规则而不是 4 条)。

验证:pnpm verify 绿(110 单测);默认档案 e2e-flow **74/74**(含 T39/T39b/T50/T70);
空库软件档案 software-flow **54/54**。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant