fix(security,approvals,metadata-core): 补上派生契约漏掉的 8 个对象的 bulk 原语 (#3026) - #3745
Conversation
…the eight objects the derivation-contract rollout missed (#3026) The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch request is admitted only when the object grants the `bulk` primitive AND the batched child operation is itself allowed. Before that, the `*Many` routes checked only the child verb, so a boilerplate CRUD-five whitelist (`['get','list','create','update','delete']`) batched fine. The companion fix — adding `bulk` wherever an explicit whitelist survived — was applied only inside `platform-objects`. Eight objects carrying the same boilerplate live in `plugin-security`, `plugin-approvals` and `metadata-core` and kept the gap: `/batch`, `createMany`, `updateMany` and `deleteMany` answered 405 OBJECT_API_METHOD_NOT_ALLOWED on objects whose single-record create/update/ delete are wide open. `data-objectstack` rethrows that 405 without falling back to per-row writes, so multi-select delete in the Setup grids failed outright. None of the eight is deliberately tightened: six are `managedBy:'config'` or `'system'` with `userActions: { create, edit, delete }`, so ADR-0103 D3 reconciliation strips nothing and the whitelist reaches the REST gate as authored; `sys_view_definition` has no `managedBy` at all. No new authority is granted — `bulk` only permits batching verbs each object already exposes one record at a time, and every batched row still passes the same row- and field-level checks. The whitelists stay explicit rather than being deleted (the #3543 treatment for equivalent-to-open boilerplate): `reconcileManagedApiMethods` early-returns on a non-array `apiMethods`, so dropping the line would silently disable the managed-write backstop on the seven `managedBy` objects. Tests were written red first — `isApiOperationAllowed(eff, 'bulk', { bulkChild })` returned false for all three children on every object before the change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CkdX2VCuKfcsFBbvtATe7V
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
#3745 的根因不是判断错,而是审计范围:#3391 P1 把 bulk 门禁改成 `bulk ∧ derived(child)` 后,配套的「补 bulk 原语」被写成了 platform-objects 包审计,而缺口在另外三个包。 本 PR 把人工排查换成扫描 packages/** 下每个 *.object.ts 的 CI 检查,断言三条不变量:有单条写就必须有批量(否则登记豁免并写理由,今天为空)、只能声明 6 个原语(legacy 动词声明了是静默死元数据)、豁免清单不许留陈旧条目;外加扫描量下限断言,防止扫不到文件时真空通过。 扫源码而非 import 全部对象包,避免反转 spec → * 依赖方向,且新包新对象自动被覆盖 —— 这次缺口的形状正是「新包没人扫」。 防假绿:拿掉 sys_permission_set 的 'bulk' 后棘轮如实变红并打印对象名、白名单与文件路径。spec 259 files / 6721 tests 全过;对最新 main 扫描 58 处声明 0 违规。
#3745 把这个对象的样板 CRUD 五件套补成六原语后,该白名单已与「无白名单」等价,只是不再跟踪未来新增的原语,故按 #3543 审计惯例回收。零行为变化:undefined 解析为 unrestricted,有效操作集与 restricted 持有全部六原语完全相同。 删这个、留 #3745 那七个不是双标:reconcileManagedApiMethods(ADR-0103 D3)在 apiMethods 非数组时早退,所以对 managedBy 对象删白名单会连带关掉 managed-write 兜底;本对象没有 managedBy,D3 本就不适用。测试改为双向守卫(白名单必须保持缺席 + resolver 必须报 unrestricted),并钉住派生得到的动词。 另按 dogfood 流程起真实服务做了派生契约端到端实测,14/14:/me/permissions 下发 apiOperations(57/57 非通配符条目被注解)、从未声明过 export/import 的对象派生放行、四个对象的 deleteMany 越过 API 门禁、收紧对象 create/import/deleteMany 仍 405、apiEnabled:false 仍 404 优先。
更正一处用户影响描述(修复本身不受影响)拿到 objectui 源码后核实:本 PR 说明里「 适配器
所以那 8 个对象缺 修复本身仍然正确、值得做:API 层的不对称是真的 —— 用真实服务打过矩阵, 前端契约那半也已在浏览器里实测:两个同为 platform 桶、UI 轴完全相同的对象,仅因服务端下发的 effective 集不同, 错误前提的出处与完整调用链分析记在 #3757 的评论里。 Generated by Claude Code |
背景
#3391 P1 把 bulk 门禁改成
bulk ∧ derived(child)(api-derivation.ts:324、rest-server.ts:6633-6729):批量请求只有在对象授予bulk原语 且被批量的子操作本身也被允许时才放行。在此之前*Many路由只查子动词,所以 CRUD 五件套样板白名单(['get','list','create','update','delete'])批量是通的。配套修复(「给还留着显式白名单的对象补
bulk原语」)当时只在platform-objects包内做了。同样样板的 8 个对象在别的包里,没被扫到 —— 于是/batch、createMany、updateMany、deleteMany在这些对象上返回 405OBJECT_API_METHOD_NOT_ALLOWED,而它们的单条 create/update/delete 完全开放。objectui 的data-objectstack适配器对这两个方法的 405 是直接 rethrow、不回退逐行写,所以 Setup 网格里多选删除会直接报错。改动
8 个对象的白名单追加
'bulk'(每处带两行注释说明门禁语义):plugin-securitysys_capability、sys_permission_set、sys_position、sys_position_permission_set、sys_user_permission_set、sys_user_positionplugin-approvalssys_approval_delegationmetadata-coresys_view_definition判据:这 8 个都不是刻意收紧
managedBy:'config'或'system'且带userActions: { create: true, edit: true, delete: true }→resolveCrudAffordances全放行 → ADR-0103 D3 的reconcileManagedApiMethods一个动词都不剥,白名单原样到达 REST gate。sys_approval_delegation同上(managedBy:'system'+ userActions 全开,注释里明确写了「RLS/权限集才是 authz」)。sys_view_definition根本没有managedBy→ platform 桶 → D3 直接早退。不新增任何权限:
bulk只是这些对象已经逐条开放的动词的批量形态,每一行仍然过同一套行级/字段级权限中间件。为什么保留显式白名单而不是整行删掉
#3543 对「等价全开」的样板白名单是整行删除(BU / BUM / user-preference)。这里不能照搬:
reconcileManagedApiMethods(registry.ts:448-449)在apiMethods非数组时早退——删掉白名单等于顺手关掉这 7 个
managedBy对象的 managed-write 兜底:今天它们靠userActions全开所以剥不掉东西、行为一致,但将来谁去掉userActions,有白名单时 D3 会剥掉 create/update/delete,没白名单时 D3 完全不跑。所以显式保留 + 补bulk。sys_view_definition没有managedBy,两种做法等价,为统一口径也保留显式声明(测试注释里写明了这一点)。测试(先红后绿)
新增 3 个测试文件,共 23 条断言:
packages/plugins/plugin-security/src/rbac-objects-bulk-exposure.test.ts(6 对象 × 3 组)packages/plugins/plugin-approvals/src/sys-approval-delegation.object.test.tspackages/metadata-core/src/objects/sys-view-definition.object.test.ts防假绿:改对象之前先跑,如实变红 ——
isApiOperationAllowed(eff, 'bulk', { bulkChild: 'create'|'update'|'delete' })在全部 8 个对象上都返回false(plugin-security 那份:12 failed | 6 passed)。改完全绿。验证
@objectstack/plugin-security31 files / 653 tests 全过@objectstack/plugin-approvals7 files / 239 tests 全过@objectstack/metadata-core8 files / 102 tests 全过tsc --noEmit三个包 0 错(plugin-approvals/src/action-link-pages.ts的replaceAll报错是裸tsc -p的 lib 配置导致的既有问题,该文件不在本 diff 内)changeset:三包
patch。关联
reconcileManagedApiMethods)、ADR-0049Generated by Claude Code