Skip to content

feat(security): 权限集「两道门」分离 — 包门发布物化 + 管理门写门 (ADR-0086 P2) - #2573

Merged
os-zhuang merged 1 commit into
mainfrom
claude/production-launch-priorities-ps8hk3
Jul 4, 2026
Merged

feat(security): 权限集「两道门」分离 — 包门发布物化 + 管理门写门 (ADR-0086 P2)#2573
os-zhuang merged 1 commit into
mainfrom
claude/production-launch-priorities-ps8hk3

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

概述

落地 ADR-0086 P2「两道门分离」 的 framework 侧,把"谁能改一个权限集"拆成两扇互不重叠、且在数据层强制的门(不再靠约定):

  • 包门(package door):权限集在包里声明/编辑 → 存成 permission 元数据草稿 → 发布时物化进 sys_permission_set
  • 管理门(admin door):环境管理员经通用数据面 CRUD 改自己的权限集,但不能碰包管理的行

本 PR 只做框架侧(块1+块2)。objectui 包门接线(把实时保存改走 mode:'draft' + 批量发布)是后续独立改动。

块1 — 包门:发布时物化(路线①,复用既有元数据流水线)

变更
metadata-protocol/protocol.ts新增通用发布期物化器注册表registerPublishMaterializer(type, fn);publishMetaItem 在 promote 后调用对应物化器,把结果作为 materializeApplied 挂在响应上(best-effort、绝不 throw——与 seedApplied 同契约)。publishPackageDrafts 批量路径聚合materializeApplied,让批量发布里被拒的集不再被静默吞掉
metadata-protocol/sys-metadata-repository.tspromoteDraft 现在返回草稿的 packageId,物化器据此盖上归属包
plugin-security注册 permission 物化器:把已发布集 upsert 进 sys_permission_set(managed_by:'package' + package_id)。单集 upsert(upsertPackagePermissionSet)与 bootstrapDeclaredPermissions共用,所以开机播种与运行时发布走同一套 own-row / 外包拒绝 / env 自建永不覆盖 规则。物化落空(无归属包 / 名字被别处占用)时报 success:false

permission 类型本就 supportsOverlay/allowRuntimeCreate/allowOrgOverride/supportsVersioning 全开,REST /meta/*?mode=draft + /meta/*/publish 已就绪——几乎零框架新增,只加了"发布→物化"这座桥。

❌ 未走路线②(给 sys_permission_set 数据行自建平行 draft):ADR-0016/0010 已多次否决,等于重复开发。

块2 — 管理门:数据层写门(硬拒绝)

  • 安全中间件拒绝任何管理门对 managed_by:'package'sys_permission_set 行的写(update/delete/transfer/restore/purge),并拒绝任何伪造managed_by:'package' 的负载(insert 或 update、单对象或数组)。
  • 放在空主体 fall-open 与 CRUD 检查之前,是一道真正无条件的数据层边界——对无主体上下文、对带 modifyAllRecords 的超级用户都成立。
  • 多行/带过滤的写,仅当过滤真正命中某个包管理行时才拒绝,所以只改 env 自建集的批量编辑照常通过(无过度封锁)。
  • 系统/开机写带 isSystem,在中间件最前端短路——播种器与物化器自身的写不会被自己的门挡住。

验证

  • 单测:物化语义(归属包盖章 / 幂等 / env 与外包不覆盖 / 无归属包跳过);写门(单/数组伪造、update 改badge 伪造、无主体上下文、精确批量命中)。plugin-security 173 passed、metadata-protocol 8 passed、objectql publish-package-drafts 10 passed。
  • dogfood(真实 showcase 内核):发布一个包权限草稿 → 断言物化出带 managed_by:'package'+package_id 的行;再经 REST 驱动管理门——改包行 403、改 env 行 200、insert 伪造 provenance 403。4 passed。
  • 全量构建绿(70/70),改动文件 lint 干净。

自审

已用多角度自审(逐行 + removed-behavior + cross-file),发现并修复:数组 insert 绕过伪造门、update 改badge 伪造、无归属包误报 success、批量发布丢弃物化结果、写门在 fall-open 之后不够无条件、批量写门过度封锁——均已修并补测。

关联

  • ADR-0086(P1 已落 package_id/managed_by/bootstrapDeclaredPermissions;本 PR 为 P2)
  • ADR-0078(inert-metadata:包集终于进管理面)、ADR-0010(admin read-mostly 现在落到数据层强制)、ADR-0049(honesty:物化落空如实上报)
  • changeset:@objectstack/metadata-protocol minor、@objectstack/plugin-security minor

🤖 Generated with Claude Code

https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT


Generated by Claude Code

@vercel

vercelBot commented Jul 4, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 4, 2026 12:15pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/dogfood, @objectstack/metadata-protocol, @objectstack/plugin-security.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/authorization.mdx(via packages/dogfood, packages/plugins/plugin-security)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/plugin-security)
  • content/docs/concepts/packages.mdx(via @objectstack/plugin-security)
  • content/docs/getting-started/cli.mdx(via @objectstack/plugin-security)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx(via packages/plugins/plugin-security)
  • content/docs/guides/metadata/dashboard.mdx(via @objectstack/plugin-security)
  • content/docs/guides/packages.mdx(via @objectstack/plugin-security)
  • content/docs/guides/plugins.mdx(via @objectstack/plugin-security)
  • content/docs/guides/security.mdx(via @objectstack/plugin-security)
  • content/docs/guides/solutions/data-automation-interface-access.mdx(via packages/plugins/plugin-security)
  • content/docs/guides/solutions/role-based-interfaces.mdx(via packages/plugins/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.

Splits who may change a permission set into two non-overlapping doors,
enforced at the data layer instead of by convention.
块1 — package door (publish-time materialization):
- metadata-protocol: generic publish-time materializer registry
(registerPublishMaterializer); publishMetaItem projects a published body
into its data-plane row and surfaces materializeApplied (best-effort,
never thrown — same contract as seedApplied). publishPackageDrafts
aggregates it across the batch so a refused set is surfaced, not swallowed.
- promoteDraft now returns the draft's packageId so the materializer stamps
the owning package.
- plugin-security registers a `permission` materializer that upserts the
published set into sys_permission_set with managed_by:'package' +
package_id. The single-set upsert (upsertPackagePermissionSet) is shared
with bootstrapDeclaredPermissions, so boot and publish apply identical
own-row / foreign-package / env-authored rules. A publish that materializes
nothing (no owning package / name owned elsewhere) reports success:false.
块2 — admin door (data-layer write gate):
- The security middleware refuses any admin-door write to a
sys_permission_set row with managed_by:'package', and refuses a payload
(insert OR update, single object OR array) that forges managed_by:'package'.
- Placed before the empty-principal fall-open and the CRUD check, so it is a
real, unconditional boundary — it holds for a principal-less context and for
a superuser with modifyAllRecords. A multi-row/filter write is denied only
when a package-owned row actually falls within the write's own filter, so
env-only bulk edits still succeed. System/boot writes carry isSystem and
bypass the middleware, so the seeder and materializer are unaffected.
Tested: unit (materialize semantics, gate incl. array-forge / update-to-forge
/ principal-less / precise-bulk) + a dogfood test booting the real showcase
stack that publishes a package permission draft and drives the admin door
against the seeded package row over REST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
@os-zhuang
os-zhuangforce-pushed the claude/production-launch-priorities-ps8hk3 branch from 8a106ad to b625994CompareJuly 4, 2026 12:09
@os-zhuang
os-zhuang marked this pull request as ready for review July 4, 2026 12:38
@os-zhuang
os-zhuang merged commit 9796e7c into mainJul 4, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/production-launch-priorities-ps8hk3 branch July 4, 2026 12:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude