Skip to content

app-showcase 权限模型复测清单(07-15 对照 main 重评):1 项回归级重大缺陷(#2930 锚点写护栏被 A4 词表打穿)+ 2 项部分修复 + 3 项未修 + 1 项建议降级 + 2 项观察 #2926

Description

@baozhoutao

背景

examples/app-showcase 权限模型(ADR-0090)做第三轮全量复测:全新 DB 起服务 → REST 49 断言 → Playwright 25 条 UI 用例。只测试、不修改代码。修复测试环境(补岗位绑定、造 announcement 数据、物化共享)后 REST 49/49、Playwright 25/25 全部通过;下列问题均为复测过程中暴露的记录,且同日已用一手证据逐条二次核验(在线 REST 重放、SQLite 物理表直查、fresh DB 副本引导、源码/规格对照)。

完整测试报告/清单/复跑脚本可经已关闭 PR #2921refs/pull/2921/head 追溯(按 #2923 政策不入库)。关联:#2920(authz 跟踪)。

📌 重评说明(2026-07-15)

原清单基于 2026-07-14 复测时的 main。当日晚间起 main 合入了大量授权改动(#2930 写护栏、#2934 A4 三态统一、B1/B2/B4 内核链、audience-binding suggestion 机制等),本次已逐项对照最新 main(framework c5e68b2 / objectui a637ab6)源码复核,结论重写如下。最重要的变化:

分级速览(重评后)

# 问题 原级别 重评结论(2026-07-15)
锚点岗位(everyone)可被数据 API 物理删除 🔴 重大 🔴 仍成立,升级为回归:#2930 护栏词表停在 system/config,A4 后落库值是 platform/package,护栏对 sys_position 完全不触发
fresh 部署绑定 0 行静默降级 🔴 重大 🟠 部分修复:everyone 基线已 boot 自动绑;7 个 showcase 岗位仍空壳;规格 "never auto-bound" 与实现矛盾仍在
启动期种子不物化共享规则 🟡 未修 🟡 仍成立,根因=物化 hook 对 isSystem 短路(rule-hooks.ts:54)
无 cookie 时 console FLS fail-open 🟡 未修 🟡 仍成立(objectui 无相关修复)
announcement 无 name/无种子/创建权窄 🟡 未修 🟢 建议降级:displayName 回退已健全;无种子+创建权窄属设计,余下仅示例数据完善
dev 服务器被外部 SIGKILL 🔵 环境 🔵 维持原判,无代码可评
拒绝语义分层不一致(400 vs 403) 🟡 未修 🟡 仍成立:抛出点已带 status=403/code=FORBIDDEN,但 REST 层 mapDataError 丢弃,净结果未修
lookup 列显示 name 而非 displayNameField 🔵 既知 🔵 仍成立
复跑脚本 perm-setup.sh 不自足 🟡 未修 🟡 部分缓解:everyone 基线绑定一步已免;其余两步半仍需手工;跟随②③⑤
REST list 静默忽略 $filter 🟡 未修 🟠 部分修复:$filter 已归一到 where;unknown-param 校验仍缺(#2873 治本项未落)

🔴 ① 锚点岗位可删 —— 已修复但被回归打穿(当前唯一 🔴)

原始现象(07-14 实测,全链路重放):managed_by: system 的 everyone 锚点无删除保护;带绑定删除得到的 409 只是 FK 引用完整性假象——先删 everyone→showcase_member_default 绑定行(200),再 DELETE everyone 岗位 → HTTP 200,锚点真删;重启后 bootstrapDeclaredPositions 只补锚点行(新 id),绑定不补,基线授权丢失。

重评(对照 main c5e68b2)——缺陷仍成立,原因已变:

  1. PR feat(plugin-security): 内置行写护栏 — sys_position / sys_capability 平台/应用托管行不可删改 #2930(closes feat(plugin-security): 内置行写护栏 — sys_position / sys_capability 平台/应用托管行可被删改 #2918,07-14 16:15 合入)新增 assertSystemRowWriteGate(packages/plugins/plugin-security/src/security-plugin.ts:598 接线,实现 ~L1958-2033),本应对托管行 delete/update fail-closed 拒绝,连 modifyAllRecords 超管也拦。
  2. 但护栏的 provenance 词表停在旧值——security-plugin.ts:133:
    sys_position: { ..., managed: { system: 'the platform', config: 'an application package' } }
  3. 而同日稍晚合入的 A4(feat(plugin-security): A4 — managed_by 三态统一 + listView 露出 (#2920) #2934)把 sys_position 词表统一为 platform/package/admin:新锚点行落库即 managed_by: 'platform'(bootstrap-builtin-positions.ts:84),且 normalize-managed-by.ts 的 boot reconciler 会把存量 system/config主动改写platform/package
  4. 结果:spec.managed['platform'] 查无 → 护栏不抛错 → DELETE 直达驱动。先删绑定绕过 FK 后,everyone 锚点今天依然可被物理删除。 sys_capability 侧词表一致(platform/package),保护有效——只有 sys_position 裸奔。
  5. 回归零告警的原因:护栏单测(security-plugin.test.ts:1282 起)仍喂 system/config 旧值,与过期的门自洽,CI 全绿。另 normalize-managed-by.ts 注释自称 "NO runtime path branches on the legacy values"——SYSTEM_ROW_PROVENANCE.sys_position 恰恰 branch 在 legacy 值上,该安全声明不成立。

"重启不自愈"半句修正:重启时 security-plugin.ts:1301-1318 会幂等重建 everyone→fallback 集这一条基线绑定(见②),但管理员手工加到 everyone 的其余绑定不恢复,锚点 id 漂移仍在。

修复(极小改动):security-plugin.ts:133 词表对齐 A4({ platform: 'the platform', package: 'an application package' },与 sys_capability 行一致),同步把 security-plugin.test.ts:1282-1435 仍用 system/config/user 的用例改为新词表;并修正 normalize-managed-by.ts 的错误安全注释。建议单独开回归 bug 引用 #2918 / #2930 / #2934

🟠 ② fresh 部署 0 绑定 —— 部分修复,三点分述

已修:everyone 基线不再空壳。fallbackPermissionSet 现在几乎恒有值——取 app 的 isDefault 集,无则兜底内置 member_default(security-plugin.ts:294-304);kernel:ready 时以 isSystem 上下文幂等绑到 everyone(L1285-1322),并新增高权位集拒绑护栏(loud warn)。fresh boot 下 sys_position_permission_set 不再为 0 行。新增 sys_audience_binding_suggestion 建议表 + listAudienceBindingSuggestions 服务(ADR-0090 D5/D9,suggested-audience-bindings.ts),"待绑定"首次有了可查询诊断面。

未修:7 个 showcase 岗位(contributor/manager/exec/auditor/ops/field_ops_delegate/client_portal_user)fresh 部署仍全部空壳。建议机制只收带 packageId 的包级声明集(suggested-audience-bindings.ts collectDeclaredSuggestions L129-136 拒绝无 packageId 的应用级集),岗位↔权限集的声明式绑定仍不存在;persona 静默降级依旧,只是 everyone 基线不再一起坏。

规格矛盾仍在,且形态更清晰:packages/spec/src/security/permission.zod.ts:187 仍逐字写 "[ADR-0090 D5] Install-time suggestion to bind this set to the everyone position (admin confirms; never auto-bound)",而 isDefault 集恰恰经 CLI appDefaultPermissionSetName(packages/cli/src/commands/serve.ts:1455-1462)解析为 fallback,在 boot 时被自动写入绑定(security-plugin.ts:1310,无管理员确认)。现在实际是双轨:应用级 isDefault → 自动绑(违背规格文案);包级 isDefault → 建议表+管理员确认(符合规格)。规格文案需改写区分两轨。绑定失败仍是 catch→warn 非致命。

🟡 ③ 种子不物化共享规则 —— 仍成立,根因已定位

种子并未绕过 hook 层:SeedLoaderServiceengine.insert/update(packages/metadata-protocol/src/seed-loader.ts:255-257 等)。真正原因是共享物化 hook 首行短路:

  • packages/plugins/plugin-sharing/src/rule-hooks.ts:54if ((ctx?.session as any)?.isSystem) return;
  • 而种子固定用 { isSystem: true, skipTriggers: true }(seed-loader.ts:699)。

全表 reconcile 入口只有手动 REST 端点(packages/rest/src/rest-server.ts:5237 evaluateRule);启动路径仅 bindRuleHooks + bootstrapDeclaredSharingRules(播种规则行,不评估既有记录),无自动 backfill。这与"admin 空触碰一次即物化"的实测现象完全吻合。

修复方向:种子通道对命中共享规则的对象放行物化,或 boot 后对 declared rules 跑一次 backfill(复用 SharingRuleService.evaluateRule)。

🟡 ④ 无 cookie 时 console FLS fail-open —— 仍成立(objectui)

  • 权限拉取只依赖 cookie:packages/permissions/src/MePermissionsProvider.tsx:80-96 用裸 fetch + credentials:'include',不注入 localStorage auth-session-token Bearer(console 其余数据层走 createAuthenticatedFetch()),Provider 也没有自定义 fetch 的口子。
  • 未知即放行:MePermissionsProvider.tsx:118 if (!objPerm) return true;表单侧 packages/plugin-form/src/ObjectForm.tsx:304-322!perms?.isLoaded 时原样返回可编辑字段。
  • 唯一 fail-closed 分支是端点返回非 2xx(data 保持 null → loadingFallback 挡住 children)。前端缺省方向仍是 fail-open;数据面 PATCH 仍会剥离受限字段,不构成越权写入,但方向应改为权限未知即只读。

🟢 ⑤ showcase_announcement —— 建议降级为示例完善项

  • name 字段属实(examples/app-showcase/src/data/objects/announcement.object.ts:36-40,仅 title/body/owner_id),但原清单担心的"显示名回退异常"已不成立:packages/spec/src/data/display-name.tsderiveDisplayField(L163-187)会命中 Tier-1 候选 title,空值 floor 到 Record #<id>,回退健全。
  • 无种子行属实(src/data/seed/index.ts 无 announcement 条目);创建权仅 showcase_ops(permission-sets.ts:186 唯一 allowCreate)——原清单已承认"mia 403 是设计使然"。
  • 余下实质仅"演示数据缺一块",建议作为 app-showcase 示例完善处理,不是框架缺陷。

🟡 ⑦ 400 vs 403 分层不一致 —— 仍成立,只差最后一公里

抛出点已修了一半:packages/plugins/plugin-sharing/src/sharing-plugin.ts:403-409 现在抛错时已带 err.status = 403; err.code = 'FORBIDDEN'

REST 层未接:数据 CRUD 路由 catch 走 mapDataError(POST rest-server.ts:3318 / PATCH :3404 / DELETE :3447),它从不读取 error.status;其 403 短路(L111-124)只认 code === 'PERMISSION_DENIED' / PermissionDeniedError / '[Security] Access denied' 前缀,均不匹配本错误 → 落默认分支 rest-server.ts:325仍 400 + 裸 error 无 code。(只有 sendError L338-349 会采纳 error.status,但 CRUD 路径不走它。)

修复:让 mapDataError 尊重已标注的 error.status/error.code(或把 FORBIDDEN code 加入 403 短路),客户端即可统一分支。

🟡 ⑨ 复跑脚本不自足 —— 部分缓解,跟随②③⑤

脚本本体按 #2923 政策不入库。三个手工步骤重评:

  1. everyone→member_default 基线绑定 —— 已免(②的 boot 自动绑定覆盖);7 条 showcase 岗位绑定仍需 REST 循环补建;
  2. announcement 测试数据仍需以 admin/oskar 造(不能带 name 字段)——跟随⑤;
  3. 共享物化触碰仍需——跟随③。

注:zsh 下补绑定不能用 declare -A(bad substitution),用 ${pair%%:*} 字符串切分。

🟠 ⑩ REST $filter —— 部分修复

已修:packages/metadata-protocol/src/protocol.ts:2352-2368 现在显式识别 $filter 并归一到 where(options.filter ?? options.filters ?? options.$filter ?? options.where;字符串先试 JSON.parse / FilterAST)。原清单里"整个 $filter 被丢弃、按名取 id 实得第一条记录"的具体危险场景已消除(OData 表达式字符串结构上进入 where,语义未必被驱动接受,但不再静默返回全量——配合 #2873 已修的驱动层 fail-loud,大概率报错而非放行)。

未修:广义的 unknown-param 校验仍缺。protocol.ts:2411-2432knownParams 白名单在 !options.where 时把未知键静默转成隐式字段等值过滤;where 已存在时未知键静默残留。无任何 4xx 或回显生效过滤条件的机制——#2873 建议的"上游统一校验"仍是缺口。

🔵 环境 / 既知观察(维持原判)

⑥ dev 服务器反复被外部 SIGKILL

受控后台监控中尾标 echo 未执行、shell 一并消失 → 外部 SIGKILL,非进程内部崩溃;与权限模型代码无关,属测试机环境问题,记录备查。

⑧ lookup 列显示 API name 而非 displayNameField

重评确认仍成立(objectui):网格只读单元格 LookupCellRenderer(packages/fields/src/index.tsx:1171)四处调用 pickRecordDisplayName 均不传 preferredField,走硬编码启发式(name 优先,L30-47);display_field 虽在关系元数据白名单里(plugin-grid/src/ObjectGrid.tsx:210-212),但只有内联选择器 LookupField 消费,只读单元格不消费。


后续动作建议(按优先级)

  1. 立即:为①开回归 bug(或直接修)——security-plugin.ts:133 词表对齐 + 更新 stale 单测,一行级修复堵回锚点删除;引用 feat(plugin-security): 内置行写护栏 — sys_position / sys_capability 平台/应用托管行可被删改 #2918 / feat(plugin-security): 内置行写护栏 — sys_position / sys_capability 平台/应用托管行不可删改 #2930 / feat(plugin-security): A4 — managed_by 三态统一 + listView 露出 (#2920) #2934
  2. ②拆两半跟进:a) showcase 岗位声明式绑定 / 应用级集进建议表(特性缺口);b) permission.zod.ts:187 "never auto-bound" 文案改写以区分应用级自动绑 vs 包级建议确认双轨(文档级)。
  3. ③⑦为根因明确、改动小的框架修复:③ hook 的 isSystem 短路对共享物化放行或 boot backfill;⑦ mapDataError 尊重 error.status/error.code
  4. ⑤降级为 app-showcase 示例完善项;④⑧归 objectui 排期;⑩的 unknown-param 校验并入 查询过滤器遇到"不认识的条件"时,应统一报错而不是悄悄放行(所有数据驱动) #2873 后续。

复跑方式

refs/pull/2921/head 检出后:bash docs/test/scripts/perm-setup.sh → 手工补 ⑨ 剩余步骤(showcase 绑定、announcement 造数、共享触碰;everyone 基线已由 boot 自动绑定覆盖)→ bash docs/test/scripts/perm-test.sh(49 断言)→ PERM_BASE_URL=... pnpm exec playwright test --config playwright.permission.config.ts(25 条)。测试账号:admin@objectos.ai;personas @example.com。

问题④的两张对比截图(R3-I2a/R3-I2b)留存本地,可按需以评论附件补充。


重评核验方法:framework origin/main c5e68b2 / objectui origin/main a637ab6 源码逐项对照(文中行号即当前 main);关联 PR/issue 状态核对(#2918 已由 #2930 修复合并、#2873 已关闭、#2920 开放跟踪中)。

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions