fix(verify,plugin-security,cli): bootStack honours the app-declared default permission set (#7001) - #7091
Conversation
…efault permission set (#7001) 两条启动路径对「应用声明的默认权限集是否存在」给出了不同答案: `objectstack serve` 会读取 `config.permissions` 中标记 `isDefault: true` 的权限集, 并作为 SecurityPlugin 的 `fallbackPermissionSet` 传入;而 `@objectstack/verify` 的 `bootStack` 直接构造了一个 vanilla `new SecurityPlugin()`,从不读取 `config.permissions`。于是应用声明的 profile 在真人执行 CLI 时生效,在该应用自己的 测试套件启动时却静默缺席 —— 这正是「declared ≠ enforced」,而且发生在专门用来捕捉 这类偏差的测试载体内部:测试全绿,生产行为却不同。 #5491 之前这一点不可见:平台的 `member_default` 带有 `object_permissions['*']` 通配符,没有任何应用 profile 的成员照样能访问所有对象,fallback 从来不承重。#5491 有意移除了这层地板,其 Migration 章节给出的唯一消费者动作 —— 通过 `isDefault: true` 提供应用默认 profile —— 恰恰是 `bootStack` 无法表达的。 解析逻辑现在只有一处,两条路径都调用它:`appSecurityPluginOptions(config)`,新增于 `@objectstack/plugin-security`,与既有的 `appDefaultPermissionSetName` 并列。它回答 启动方真正的问题 —— 「这份 config 该给 SecurityPlugin 构造函数传什么」—— 而不只是 名字,因为后半截 `name ? { fallbackPermissionSet: name } : undefined` 是一个决策而非 格式选择:serve.ts 曾把它写死在原地,而 bootStack 压根没长出来过。serve.ts 一并收敛 到同一个 helper,两条路径从此按构造一致,而不是靠各自记得。 行为变化仅限 `@objectstack/verify`:对声明了 `isDefault` 权限集的应用, `bootStack(config)` 现在以该 profile 作为每请求可加性基线(ADR-0090 D5),与 `objectstack dev` 一致;未声明的应用完全不受影响(解析返回 `undefined`,插件继续从 内置集推导 `member_default`)。刻意需要平台原生基线的套件现在显式表达: `bootStack(config, { security: new SecurityPlugin() })`;`opts.security` 传入的实例 整体胜出,永不被合并改写。 反向验证(两个方向都按预测): - 还原 serve.ts 的原地写法 → parity 契约测试 3 红,verify 自身 6 绿(除该契约外, 仓库里没有任何东西盯着 serve 这一侧)。 - 还原 harness.ts → parity 2 红 + verify 2 红。该消融还暴露出扫描本身的弱点: 未使用的 import 让 `toContain('appSecurityPluginOptions')` 保持绿,故断言改为 测量构造式而非字符串。 实测影响面:dogfood 86 个文件 / 524 个用例中,仅 1 条断言移动 —— `me-apps-and-everyone-baseline`,其文件头本就写着「Deliberately VANILLA」。该依赖 真实存在但此前只由 harness 默认值静默表达,现在写进参数里。#5491 时已手工搭建 `test/showcase-security.ts` 来补这个洞的 showcase 夹具不受影响。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 27 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #7001
问题
两条启动路径对「应用声明的默认权限集是否存在」给出了不同答案:
objectstack serve认它 —— 读取config.permissions中标记isDefault: true的权限集,作为 SecurityPlugin 的fallbackPermissionSet传入。bootStack不认 ——packages/verify/src/harness.ts直接构造 vanillanew SecurityPlugin(),从不读config.permissions。于是应用声明的 profile 在真人跑 CLI 时生效,在该应用自己的测试套件启动时静默缺席。这是
declared ≠ enforced,而且发生在专门用来捕捉这类偏差的测试载体内部:测试全绿,生产行为却不同。两个锚点已在当前
origin/main复核(卡片给的serve.ts:2008有漂移,实际调用在 2012;harness.ts在 419)。#5491 之前这一点不可见:
member_default带object_permissions['*']通配符,没有任何应用 profile 的成员照样能访问所有对象,fallback 从不承重。#5491 有意移除了这层地板,其 Migration 章节给出的唯一消费者动作 —— 通过isDefault: true提供应用默认 profile —— 恰恰是bootStack无法表达的。做法
解析逻辑收敛到一处,两条路径都调用:
appSecurityPluginOptions(config),新增于@objectstack/plugin-security,与既有的appDefaultPermissionSetName并列。它回答启动方真正的问题 —— 「这份 config 该给 SecurityPlugin 构造函数传什么」—— 而不只是名字。后半截
name ? { fallbackPermissionSet: name } : undefined是一个决策而非格式选择:serve.ts 曾把它写死在原地,而 bootStack 压根没长出来过。返回undefined而非{ fallbackPermissionSet: undefined }同样是刻意的 —— 前者让构造函数走自己的内置推导,后者只是碰巧命中同一分支,离「静默关掉平台基线」只差一次重构。serve.ts一并收敛到同一个 helper,两条路径从此按构造一致,而不是靠各自记得。为什么不做更深的改法
考虑过让 SecurityPlugin 在
start()自己去 metadata registry 里找isDefault权限集(那样连调用方都不必记得)。没有采纳,两个理由:fallbackPermissionSet在init()就被注册成一个字符串服务,是快照,晚解析改不动它;而且按 helper 自己的文档,ADR-0090 P2 会用绑定到everyone的安装期建议整体替换这套 fallback 机制 —— 在一个即将被替换的机制上做架构改造是错的投资。收敛两条路径到一个 helper 是合适的尺寸。显式退出
opts.security传入的实例整体胜出,永不被合并改写 —— 它到达时自带构造选项,悄悄改写其中一项会是比本次修复的 bug 更糟的意外。所以刻意需要平台原生基线的套件现在显式表达:反向验证(两个方向都先定方向再跑)
消融 B 还暴露出扫描本身的弱点:还原后未使用的
import仍在,toContain('appSecurityPluginOptions')保持绿 —— 提到不等于调用。断言因此改为测量构造式而非字符串。这条是实测出来的,不是推理出来的。初始红也是诚实的红:wiring 为
member_default而非声明值,行为侧返回 403(#5491 之后内置基线不再授予任何对象权限),即应用自己声明的默认 profile 授予的读被拒。实测影响面
dogfood 86 个文件 / 524 个用例中,仅 1 条断言移动:
me-apps-and-everyone-baseline断言 bootstrap 把member_default绑到everyone锚点,其文件头本就写着「Deliberately VANILLA」。该依赖真实存在但此前只由 harness 默认值静默表达,现在写进参数里 —— 正是本 issue 要的「显式退出而非静默默认」。断言本身一字未改。#5491 时已手工搭建
test/showcase-security.ts(即这些 dogfood 应用本要证明不必要的「自定义 security 代码」)来补这个洞的 showcase 夹具,走opts.security,完全不受影响。证据:全套 dogfood 85 passed / 1 skipped / 521 tests passed。 所以是单方面翻转默认值,不需要逐套件判断。
验证
@objectstack/verifytest@objectstack/plugin-securitytest@objectstack/clitestpnpm lintcheck:闸门check:type-check-debt起初报@objectstack/verifyTEST_DEBT 8 → 9(+1)。查到是新测试文件的相对 import 少了.js后缀(NodeNext 下不解析,符号全变any—— AGENTS.md 点名的那一类)。修因而非抬棘轮:补上后缀,计数回到 8,闸门转绿。范围
docs/adr/**与content/docs/releases/**。data-service.mdx)无文件重叠,已确认。🤖 Generated with Claude Code
https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
Generated by Claude Code