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-security sys_capabilitysys_permission_setsys_positionsys_position_permission_setsys_user_permission_setsys_user_position
plugin-approvals sys_approval_delegation
metadata-core sys_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 非数组时早退——

const methods = (schema as any).enable?.apiMethods;
if (!Array.isArray(methods) || methods.length === 0) return schema;

删掉白名单等于顺手关掉这 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

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 1:24am

Request Review

@github-actions github-actions Bot 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 main Jul 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 优先。

Copy link
Copy Markdown
Contributor Author

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

拿到 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants