Repository navigation
Description
在 #6599 的消费方普查里撞到的(那张卡问的是「谁在调 /meta/_drafts」;顺着 usePublishAllDrafts 的 Publish 动作走到了这条写面)。未修,按 PD #10 立卡。
实测:一个零能力的已认证调用方能做什么
不是「没看到能力门禁」这种静态观察 —— 直接驱动 dispatcher 量的。调用方上下文 { userId: 'u_portal', systemPermissions: [] },一个会话、一条能力都没有:
### zero-capability caller context ### {"userId":"u_portal","systemPermissions":[]}
### HTTP status ### 200
### publishPackageDrafts invoked? ### 1
### drafts promoted to ACTIVE ### ["app.hr:account","app.hr:salary_review"]
### body ### {"success":true,"data":{"success":true,"publishedCount":2,"failedCount":0,
"published":[{"type":"object","name":"account"},
{"type":"object","name":"salary_review"}],"failed":[]}}
同一个调用方,打隔壁那条同族路由 POST /metadata/_migrate-stored:
### _migrate-stored status for the SAME caller ### 403
### migrateStoredMetadata invoked? ### 0
同一个调用方、同一次运行、同一类操作 —— 一条 200 并真的把整包 draft 转正,一条 403 且被调函数一次都没进。
代码面
POST /packages/:id/publish-drafts → packages/runtime/src/domains/packages.ts:160。
整个 packages.ts(739 行)里 systemPermissions / isSystem / manage_metadata / FORBIDDEN / 403 的命中数是 0。挂载侧 mountPackagesRoute(packages/runtime/src/dispatcher-plugin.ts:984)也只是 dispatcher.dispatch(...) 的转发,没有包任何门禁。所以这条路由之上除全局 requireAuth 外没有任何判据。
对照物就在同一个 dispatcher 里:_migrate-stored(domains/meta.ts)显式判 ec?.isSystem || systemPermissions.has('manage_metadata'),并且在解析 protocol 之前判,理由写在注释里(不让调用方拿 501/200 的差别去探测)。
这条路由做的事
publishPackageDrafts 把该 package 名下每一条 pending DRAFT 行提升为 active;seed 类型的 draft 被发布即意味着装载数据行(路由里 applyPublishedSeeds 那段);随后还会清掉 ADR-0045 的发布门 —— #4829 / PR #6942 刚把它挪到机器管理键 app._unpublished,由 publish-drafts 置 false。也就是说这一次调用同时是「schema 生效」「数据落库」「半成品应用对真实用户可见」。ADR-0045 自己写过这个门的失败方向判断:门失败开放会把半成品应用静默暴露给真实用户,比过度限制严格更糟(docs/adr/0045-...md:192)。
为什么它值得单独一张卡
它和 #6603 是同一形状 —— 而 #6603 刚被维护者裁为需要 manage_metadata,判词是「能写 schema 的人就该是能看见完整 schema 的人」(评论 5225531464)。#6603 管的是单条 PUT /meta/:type/:name;这条是整包批量转正,按同一条判据赌注更大,却是两条里没有门禁的那条。
同族参照:#6920(匿名 mass assignment)、#6599(_drafts 读面,其所述字段级泄露已实测证伪,见 PR #7014)。
不在本卡里替维护者选修法
至少有「照 #6603 判 manage_metadata」和「按 package 所有权/作者身份判」两种读法,成本与语义不同;而且 /packages 域下同样裸奔的还有 discard-drafts / revert / rollback / enable / disable,要不要一起收口是域级决定,不是这条路由的局部决定。留给分诊与维护者。
复现
驱动 HttpDispatcher.handlePackages('/app.hr/publish-drafts', 'POST', {}, {}, { request: {}, executionContext: { userId: 'u_portal', systemPermissions: [] } }),protocol 侧放一个 publishPackageDrafts 双档记录被提升的条目;同一上下文再驱动 handleMetadata('_migrate-stored', ctx, 'POST', {}, {}) 作对照。上面两段输出即为该探针的原始 stdout。
立卡时的查重说明
开卡前的 issue 关键词查重被账号级 REST 速率限制挡住(本班次共享身份,多次退避重试仍 API rate limit already exceeded)。改用本地信号做的查重:git log -i --grep=publish-drafts 与全仓 publish-drafts + 权限关键词交叉搜索,命中的都是发布语义(ADR-0045 可见性、seed 装载、org 作用域、端点发布门),没有一条关于这条路由的授权。若分诊发现已有同族卡,按重复合并即可。
Activity
os-project-manager commentedon Aug 9, 2026
只读普查回报(不落门、无 PR)
基线
origin/main=c3f491626。objectui HEAD =0cf8f0f。下面每条都标了 实测 / 静态阅读,两者不混写。
0. 先复现:卡片断言成立,而且比卡片更强
实测。 临时 vitest 探针驱动
HttpDispatcher(与仓内既有能力门测试packages/runtime/src/domains/meta-migrate-stored.test.ts同一驱动法),protocol 侧全是vi.fn()双档并记录调用顺序。原始 stdout:卡片说的「同一调用方,一条 200 一条 403」复现成功。
但还有一档更低的,卡片没量到:
实测: 走完整
dispatch()(resolveRequestScope→ 身份解析 →enforceAuthGate→enforceProjectMembership→ 域注册表),一个连userId都没有、身份解析产出principalKind: 'guest'的调用方,打 publish-drafts 仍得 200 并真的调进publishPackageDrafts;同一管线上_migrate-stored得 401。mountPackagesRoute(packages/runtime/src/dispatcher-plugin.ts:984-1001)直接server.post挂dispatcher.dispatch,不经过mountRouteOnServer那道 401 门(dispatcher-plugin.ts:180-223,那道门只服务插件发出的RouteDefinition,判据是route.auth !== false && !user)。探针已删除、未提交、无分支推送。
1. 今天谁在调这条路由
objectstack(搜索面:全仓 ts/tsx/mts/mjs/js/json/yaml/mdx/md,关键词
publish-drafts/publishDrafts/publishPackageDrafts,排除 node_modules、dist、CHANGELOG)静态阅读。 生产调用方只有 1 个:
packages/client/src/index.ts:1270—— SDKpackages.publishDrafts(id, opts?),POST {baseUrl}{route}/{id}/publish-drafts。而这个 SDK 方法在两仓内都没有任何生产消费方:objectstack 内唯一引用是
packages/client/src/packages-lifecycle.test.ts:42(路由绑定测试);objectui 全仓搜packages.publishDrafts/.publishDrafts(零命中。搜了但没命中的地方(也是数据):
packages/cli/src——publish-drafts/publishDrafts零命中。os没有任何命令打这条路由。examples/(app-showcase、app-todo 等)—— 零命中。packages/qa/(dogfood 全部 test)—— 零命中。docs/qa/platform-checklist/areas/studio-authoring.json:310/344/346把它写成人工步骤,该条目automated.ref指向的packages/qa/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts自己不打这条路由。objectui(HEAD
0cf8f0f)静态阅读。 生产调用方 4 处,全在
packages/app-shell,全是裸fetch('/api/v1/packages/:id/publish-drafts'),都不走 SDK:packages/app-shell/src/views/metadata-admin/PackagesPage.tsx:359—— Studio 的 Packages 详情页 Publish 按钮。packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:561—— Studio 设计面。packages/app-shell/src/preview/usePublishAllDrafts.ts:76—— 共享 hook,两个宿主:console/home/HomePage.tsx:134(Home 的 pending-drafts 横幅)与preview/DraftPreviewBar.tsx:31(ADR-0037 草稿预览条)。packages/app-shell/src/console/ai/AiChatPage.tsx:2124—— AI 会话里的 Publish 按钮;并且在getRuntimeConfig().features.autoPublishAiBuilds打开时,由ChatbotEnhanced的autoPublishDrafts自动触发(AiChatPage.tsx:2220),无人点击。packages/plugin-chatbot/src/ChatbotEnhanced.tsx只声明onPublishDrafts回调,自己不 fetch —— 不是独立调用方。搜了但没命中: objectui
e2e/、apps/、examples/搜publish-drafts零命中。未普查
cloud仓 —— 本会话未挂载该仓,没搜。2. 每个调用方各自持有什么能力
静态阅读(除标注处外)。
Studio 两处(PackagesPage、StudioDesignSurface):
挂在
studioapp 下 ——packages/platform-objects/src/apps/studio.app.ts:54requiredPermissions: ['studio.access'];Packages 页是同文件:114的componentRef: 'developer:packages'。app 级requiredPermissions由/me/apps落地(packages/rest/src/rest-server.ts:2621/:2672:不满足就把整个 app 从列表里丢掉)。所以进得了这两处 UI 的人必然持有studio.access。出厂权限集里
studio.access只有admin_full_access一个集合授予(packages/plugins/plugin-security/src/objects/default-permission-sets.ts:120-129),而这个集合同时带manage_users、manage_metadata、manage_platform_settings、manage_sharing、setup.access、setup.write。Console 三处(Home 横幅、DraftPreviewBar、AiChatPage):
不在任何 app 的
requiredPermissions之下,代码里也没有任何能力判断。HomePage.tsx:128-160的PendingDraftsBanner唯一的显示条件是client.listDrafts?.({})返回条数大于 0。那个
listDrafts打的是GET /meta/_drafts—— 实测:零能力调用方({userId:'u_portal', systemPermissions:[]})得 200,正常返回 draft 列表。所以 Console 这三处对任何已认证用户都可达。出厂的其余权限集:
admin_full_accessmanage_users,manage_metadata,manage_platform_settings,manage_sharing,setup.access,setup.write,studio.accessdefault-permission-sets.ts:120-129organization_adminmanage_org_users,setup.access,setup.writedefault-permission-sets.ts:190(注释明说故意不给studio.access与manage_metadata)member_defaultsystemPermissions键)default-permission-sets.ts:313起showcase_ops(示例)setup.access,showcase.export_dataexamples/app-showcase/src/security/permission-sets.ts:190GET /packages得 200(拿得到 package id 清单),然后POST /packages/:id/publish-drafts得 200。整条链不需要任何 UI、不需要看见任何按钮。3. 三条候选门各自会 403 掉谁
前提:出厂默认权限集 + 上面那份调用方清单。
(a)
manage_metadata(与 #6603 已裁的同一道门)organization_admin和member_default这两类人 —— 这三处今天对他们可达。studio.access,出厂只有admin_full_access授予它,而该集合同时带manage_metadata。(b)
setup.write(或同级发布类能力)admin_full_access和organization_admin。member_default这一类(以及任何未被授予上述集合的用户)。organization_admin照旧能整包发布。(c) 专设一个新的发布能力
admin_full_access(以及/或被bootstrapSystemCapabilities播种)之前,它会 403 掉上面全部 5 个现有调用方,包括平台管理员 —— Studio 的 Publish 按钮当天就坏。manage_sharing的注释(packages/spec/src/security/capabilities.ts:52-60)写明「Seeded intoadmin_full_accessso existing admin flows are unchanged」。admin_full_access,它对现有调用方的拦截面就与 (a) 完全一致。(a) 不拦下任何 Studio 调用方;(c)(播种进
admin_full_access后)同样不拦。 两者唯一改变的,是 Console 那三处对非平台管理员的可达性。(b) 连organization_admin都不拦,只拦member_default。4. 兄弟路由
POST /metadata/_migrate-stored的机制与判据静态阅读 + 实测信封。 两道门叠着:
① 域级匿名门 ——
packages/runtime/src/domains/meta.ts:163-177,handleMetadataRequest的第一条语句:实测状态码 401。
② 路由级能力门 ——
packages/runtime/src/domains/meta.ts:460-476,判据就一行:实测信封:
{ code: 'PERMISSION_DENIED', httpStatus: 403 },且migrateStoredMetadata调用次数 0。照抄时要一起抄的三个姿态:
deps.resolveService(_context, 'protocol')之前。注释写明理由:不让调用方拿 501/200 的差别去探测部署装没装这个能力。isSystem旁路 —— 引擎自调用绕过,与actionPermissionError一致。packages/runtime/src/domains/meta-migrate-stored.test.ts:44-93四个用例:无能力 403 且被调函数一次没进、持能力放行、isSystem旁路、匿名 401。对照面(实测 grep):
packages/runtime/src/domains/packages.ts全文 739 行,systemPermissions|isSystem|manage_metadata|FORBIDDEN命中数 0;并且没有shouldDenyAnonymous。全仓shouldDenyAnonymous的调用点是/meta、/actions、/automation、/ai、/security五个域加 REST 层 ——/packages不在其中。packages/qa/dogfood/test/authz-conformance.matrix.ts里有anonymous-deny-meta/-actions/-automation三条,没有/packages的任何条目。5. ADR-0045 那一环:清
app._unpublished发生在哪一步、能否分开发生在哪一步 —— 实测
单次 publish-drafts 调用内的真实调用顺序(探针记录的 trace):
即:第 4 步,严格在 draft 转正与 seed 灌入之后。
静态阅读(
packages/runtime/src/domains/packages.ts:230-301):这一步在自己的 try/catch 里,失败只写result.unhideError加一条logger.error,路由照样返回 200;成功的名字累积进result.unhiddenApps。能否与 schema 激活 / seed 灌入分开
在这条路由上不能。 没有任何开关或参数让调用方只做其中一件事;三件事在一次 POST 里绑死。(实测:body 里除
actor外没有被读的键;静态阅读packages.ts:160-172确认只从 body 取actor。)在代码结构上是分开的。 第 4 步只用
getMetaItems/saveMetaItem两个 meta 原语,不读前两步的返回值,是一个可独立摘出的块。而且这一步单独另有一条门径 —— 路由自己的错误文案就写着(
packages.ts:291):可以直接PUT /meta/app/:name带{"_unpublished": false}补做。实测:零能力调用方打PUT /meta/app/hr_console带{_unpublished:false}得 200、saveMetaItem被调用 1 次;同一条路由匿名打得 401。也就是说,在当前
origin/main(c3f491626)上,单独做 ADR-0045 翻转这条路径同样没有能力门 —— finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 裁下的manage_metadata门尚未落到 main(domains/meta.ts全文manage_metadata唯一命中就是_migrate-stored那处)。metadata.getObject,seedApplied返回{success:false, error:"this.metadata.getObject is not a function"}。已实测到的是:applyPublishedSeeds被这个零能力调用进去了、并已读回 seed body(trace 第 2 步);没实测真实 driver 上的插入行数。PD #10 —— 普查中撞到的同形无门写面(按令记在此,未另立卡)
同一次实测、同一个零能力调用方
{userId:'u_portal', systemPermissions:[]}:POST /packages/:id/discard-drafts→ 200,discardPackageDrafts被调用。这是破坏性的:丢掉该 package 名下每一条 pending draft。GET /packages/:id/export→ 200,把该 package 名下 27 种 metadata 类型整包读出来(trace 里 27 次getMetaItems)。GET /packages→ 200(package id 枚举面,正是上面那条攻击链的第一步)。PUT /meta/app/:name带{_unpublished:false}→ 200(见第 5 条)。其余⚠️ 这些非 200 全部是我的 double 缺服务方法,不是授权拒绝 ——
/packages写面在我的 double 下没跑到底:revert503 /rollback501 /duplicate501 /adopt-orphans501 /enable·disable·DELETE /:id·PATCH /:id500 /POST /packages400。packages.ts全文授权判据数为 0,所以它们同样没有门,只是未实测到底,不要读成「这些已经被拦住了」。探针
临时 vitest 文件
packages/runtime/src/zz-probe-7023.test.ts(一次性 worktree 内),驱动HttpDispatcher.handlePackages/handleMetadata/dispatch,protocol 侧全vi.fn()双档并记录调用顺序。上面所有###行为其原始 stdout。已删除、未提交、未推分支、worktree 已移除。本评论不含修法建议 —— 门的选择留给维护者。
🤖 Generated with Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
普查的四条 out-of-scope 发现已分流(PD #10)
普查评论(
5231013640)末尾记了四条不属于本卡的发现,现分流如下,本卡范围不变:POST /packages/:id/discard-drafts零能力 200(破坏性)GET /packages/:id/export零能力 200GET /packages零能力 200(枚举面)PUT /meta/app/:name带{_unpublished:false}零能力 200前三条合并为一张卡的理由
它们与本卡
publish-drafts共用同一根因(/packages域级无授权判据 + 不在authz-conformance.matrix.ts里)和同一修法。拆成三条会让三个 PR 各自在packages.ts里加一段形状略有差异的门。#7033 记的是那条结构事实:packages.ts739 行授权判据命中数 0,且该域没有shouldDenyAnonymous,而/meta/actions/automation/ai/security五个域都有。第四条为什么不另立卡(已核,非假设)
普查走的是 dispatcher 管线(
packages/runtime/src/domains/meta.ts),不是packages/rest/src/rest-server.ts。#7027(#6603)只在 rest 侧给PUT /meta/:type/:name落门,不碰 dispatcher —— 所以这一条不会被 #7027 关掉。但它正落在 #7019 的范围内:#7019 的三条肢体之一就是 dispatcher 的
PUT(实测在meta.ts:152-159,卡里原记的:264是读路径,已在 #7019 更正)。#7019 落地后该路由对零能力调用方即为 403。⇒ 重复,不另立卡。本卡状态未变
维护者现行指令:先只派普查,不落门。普查已交付,三条候选门各自会 403 掉谁的实测对照在上一条评论里。本席不代裁门,等维护者定向。
cloud仓未挂载(add_repo两次被拒),调用方普查不覆盖该仓 —— 任何门都可能 403 掉一个未被普查到的调用方。Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
认领(dev agent)
session_017uFVNMmTxLpmfQYiuKM1Yxclaude/issue-7033-packages-authz/packages域整体没有授权判据 ——discard-drafts(破坏性)、export、GET /packages零能力调用方均实测 200,#7023 只是其中一条 #7033 同根因,合并为一个 PR 一并关闭(Fixes #7033且Fixes #7023)。落地维护者 2026-08-09 已裁的/packages域授权门。Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
认领 + 状态:本卡正在被处理,不是无人管
此前只跑了只读普查(评论
5231013640),普查按约定未认领 issue —— 所以它看起来无人认领。现补上认领并说明归属,消除误会。归属:#7023 与 #7033(
/packages域整体无授权)同根因、同文件,由一个 PR 一起关。该 PR 正在飞:claude/issue-7033-packages-authz(checkpoint97c875715,PM 在首个 dev 撞账号 session 限额后打的 wip 备份并已推送)Fixes #7033且Fixes #7023session_017uFVNMmTxLpmfQYiuKM1Yx#7023 对应的门(维护者已裁):
POST /packages/:id/publish-drafts及其破坏性同族discard-drafts要求has('manage_metadata');读路由(GET /packages、export)走域级shouldDenyAnonymous+ D4 读豁免常量。每个 transport 都上门(REST + dispatcher)。维护者现行裁定:保持 #7023 与 #7033 合并、整单顶到最高优先级(插队),绿了立即翻牌 —— 不拆。
草稿 PR 一开我会回帖贴号。
Generated by Claude Code
feat(runtime,rest): /packages 域补齐授权门 —— 写要 manage_metadata、读要 D4 读集、全…
feat(runtime,rest): /packages 域补齐授权门 —— 写要 manage_metadata、读要 D4 读集、全…
feat(runtime,rest): /packages 域补齐授权门 —— 写要 manage_metadata、读要 D4 读集、全…
os-project-manager commentedon Aug 9, 2026
已随 PR #7083 合入,本卡关闭
/packages域授权门已落地 main(b295e4b36),#7033 与 #7023 由同一 PR 一起关。#7023 对应的落地事实(实测
origin/main):packages/runtime/src/domains/packages.ts现有 7 处manage_metadata/shouldDenyAnonymous命中,POST /packages/:id/publish-drafts及其破坏性同族discard-drafts要求manage_metadata,读路由走 D4 读集 + 全域shouldDenyAnonymous,两 transport(dispatcher + REST direct-mount)各一份门,门在协议/服务解析之前(拒绝不写不删、防指纹)。pin 已进packages-capability-gate.test.ts与package-envelope.conformance.test.ts。遗留盲区(已在 PR 正文与 changeset 写明):
cloud仓未挂载(add_repo两次被拒),调用方普查不覆盖该仓 —— 落地后若在cloud内发现今天不持manage_metadata/D4 读集的生产调用方,可能被此门 403,需补一次cloud普查复核。此外 #7020 记录的「门能力集 ≠ ADR-0106 D4 掩码豁免集」对齐方向仍归维护者。Generated by Claude Code
fix(metadata-core): derive the ADR-0106 D4 read exemption from the ob…