Skip to content

fix(security,approvals,metadata-core): 补上派生契约漏掉的 8 个对象的 bulk 原语 (#3026) - #3745

Merged
os-zhuang merged 1 commit into
mainfrom
claude/ui-button-api-whitelist-consistency-8ftbmc
Jul 28, 2026
Merged

fix(security,approvals,metadata-core): 补上派生契约漏掉的 8 个对象的 bulk 原语 (#3026)#3745
os-zhuang merged 1 commit into
mainfrom
claude/ui-button-api-whitelist-consistency-8ftbmc

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

#3391 P1 把 bulk 门禁改成 bulk ∧ derived(child)(api-derivation.ts:324rest-server.ts:6633-6729):批量请求只有在对象授予 bulk原语 且被批量的子操作本身也被允许时才放行。在此之前 *Many 路由只查子动词,所以 CRUD 五件套样板白名单(['get','list','create','update','delete'])批量是通的。

配套修复(「给还留着显式白名单的对象补 bulk 原语」)当时只在 platform-objects 包内做了。同样样板的 8 个对象在别的包里,没被扫到 —— 于是 /batchcreateManyupdateManydeleteMany 在这些对象上返回 405 OBJECT_API_METHOD_NOT_ALLOWED,而它们的单条 create/update/delete 完全开放。objectui 的 data-objectstack 适配器对这两个方法的 405 是直接 rethrow、不回退逐行写,所以 Setup 网格里多选删除会直接报错。

改动

8 个对象的白名单追加 'bulk'(每处带两行注释说明门禁语义):

对象
plugin-securitysys_capabilitysys_permission_setsys_positionsys_position_permission_setsys_user_permission_setsys_user_position
plugin-approvalssys_approval_delegation
metadata-coresys_view_definition

判据:这 8 个都不是刻意收紧

  • 6 个是 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 非数组时早退——

constmethods=(schemaasany).enable?.apiMethods;if(!Array.isArray(methods)||methods.length===0)returnschema;

删掉白名单等于顺手关掉这 7 个 managedBy 对象的 managed-write 兜底:今天它们靠 userActions 全开所以剥不掉东西、行为一致,但将来谁去掉 userActions,有白名单时 D3 会剥掉 create/update/delete,没白名单时 D3 完全不跑。所以显式保留 + 补 bulksys_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.ts
  • packages/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-security 31 files / 653 tests 全过
  • @objectstack/plugin-approvals 7 files / 239 tests 全过
  • @objectstack/metadata-core 8 files / 102 tests 全过
  • tsc --noEmit 三个包 0 错(plugin-approvals/src/action-link-pages.tsreplaceAll 报错是裸 tsc -p 的 lib 配置导致的既有问题,该文件不在本 diff 内)

changeset:三包 patch

关联


Generated by Claude Code

…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
@vercel

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 28, 2026 1:24am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/plugin-approvals, @objectstack/plugin-security.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx(via @objectstack/plugin-approvals)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-core)
  • content/docs/deployment/cli.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-approvals, @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-approvals, @objectstack/plugin-security)
  • content/docs/releases/v12.mdx(via @objectstack/metadata-core)
  • content/docs/releases/v9.mdx(via @objectstack/plugin-approvals)
  • content/docs/ui/audience-based-interfaces.mdx(via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx(via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 02:01
@os-zhuang
os-zhuang merged commit db48ad5 into mainJul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/ui-button-api-whitelist-consistency-8ftbmc branch July 28, 2026 02:02
os-zhuang added a commit that referenced this pull request Jul 28, 2026
#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 违规。
os-zhuang added a commit that referenced this pull request Jul 28, 2026
#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 优先。
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

更正一处用户影响描述(修复本身不受影响)

拿到 objectui 源码后核实:本 PR 说明里「data-objectstack rethrows that 405 without falling back to per-row writes, which surfaced as a hard error on multi-select delete in the Setup grids」说大了

适配器 bulkDelete 确实 rethrow,但上游两层各自兜住了:

  • 唯一调用方 useBulkExecutorexecuteBulkBatch(packages/core/src/actions/bulkFastPath.ts:93-110)对 bulkCall 的任何异常都 catch 并回退逐行;
  • console 对象网格的多选删除(packages/app-shell/src/hooks/useObjectActions.ts:96-100)是 Promise.allSettled 的逐行单删,根本不走批量路由

所以那 8 个对象缺 bulk 的实际表现是静默降级为逐行删除,不是硬报错。

修复本身仍然正确、值得做:API 层的不对称是真的 —— 用真实服务打过矩阵,deleteMany 在这些对象上确实 405 而同样的单条写完全开放,/batch 也不可用。这是 declared ≠ enforced 的另一面(白名单声称的和自动 API 实际admit的对不上),补齐后契约自洽。只是它不是一个用户可见故障。

前端契约那半也已在浏览器里实测:两个同为 platform 桶、UI 轴完全相同的对象,仅因服务端下发的 effective 集不同,New/Import 按钮一显一隐(详见 #3772)。

错误前提的出处与完整调用链分析记在 #3757 的评论里。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude