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

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:324、rest-server.ts:6633-6729):批量请求只有在对象授予 bulk 原语 且被批量的子操作本身也被允许时才放行。在此之前 *Many 路由只查子动词,所以 CRUD 五件套样板白名单(['get','list','create','update','delete'])批量是通的。

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

改动

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

包 对象
plugin-security sys_capability、sys_permission_set、sys_position、sys_position_permission_set、sys_user_permission_set、sys_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 完全不跑。所以显式保留 + 补 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.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.ts 的 replaceAll 报错是裸 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,但上游两层各自兜住了:

  • 唯一调用方 useBulkExecutor → executeBulkBatch(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

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…e apiMethods affordance rule (objectstack-ai#7934) (objectstack-ai#8314)

* test(platform-objects): sweep code-shipped managed objects through the apiMethods affordance rule (objectstack-ai#7934)

`os lint` walks an authored stack, so this repo's own `.object.ts` definitions
were never covered by `object/managed-api-method-unaffordable` (objectstack-ai#7521). Import
the exported `validateManagedApiMethods` and run it over every code-shipped
object in the monorepo — the predicate is reused, never re-derived.

Measured: 76 object files, 51 in-scope managed objects, ZERO findings. The
divergence class does not currently exist in this repo, so this lands as
regression insurance rather than a fix.

Repo-wide rather than scoped to the two packages the card names: those hold
30 of the 51 in-scope objects, and an audit scoped to a package name is the
failure already recorded in objectstack-ai#3745 and objectstack-ai#7802.

- declares the cross-package input radius (gate + turbo.json), without which
  turbo replays a stale green for a diff touching another package's objects
- platform-objects tsconfig gains `types: ["node"]` so the test layer stays at
  its TEST_DEBT number (3) instead of drifting up

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH

* test(platform-objects): resolve @objectstack/lint to source in the affordance sweep (objectstack-ai#7934)

`check:test-source-alias` went red on this branch: adding `@objectstack/lint`
as a devDependency made it a NEW unaliased artifact import for this package.
Unaliased, `validateManagedApiMethods` resolved through `exports` to
`lint/dist`, so the sweep was a verdict about build state rather than about
the rule in the checkout — acutely wrong here, since the sweep's whole purpose
is to run the CURRENT rule over the CURRENT objects, and a stale rule narrows
the population silently while the sweep keeps reporting zero findings.

This package had no `vitest.config.*` at all, so the fix is the package's
first one. It carries the alias and nothing else: no `test` block, so suite
discovery stays on the vitest defaults it ran on before (17 files / 351 tests,
unchanged) and this file's only effect is the resolution.

Array form with an anchored `/^@objectstack\/lint$/`, which is load-bearing
rather than stylistic: `@objectstack/lint` exports a second subpath
(`./runtime`), and the object form matches by PREFIX, so a bare key with a
FILE replacement would swallow it and resolve to `…/src/index.ts/runtime`
(ENOTDIR at run time). Same shape as `packages/rest` (objectstack-ai#7955) and
`service-storage` (objectstack-ai#7778).

The registry in `scripts/check-test-source-alias.mjs` is untouched — it is
shrink-only, and its own message says widening it is not the fix.

Verified: sweep census unchanged at 76 files / 51 in-scope / 9 packages, zero
findings, now measured against lint's source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH

---------

Co-authored-by: Claude <noreply@anthropic.com>
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