Skip to content

feat(plugin-security): A4 — managed_by 三态统一 + listView 露出 (#2920) - #2934

Merged
os-zhuang merged 4 commits into
mainfrom
claude/authz-a4-managed-by-tristate
Jul 15, 2026
Merged

feat(plugin-security): A4 — managed_by 三态统一 + listView 露出 (#2920)#2934
os-zhuang merged 4 commits into
mainfrom
claude/authz-a4-managed-by-tristate

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

A4 — 权限来源三态统一 + 露出(framework 侧)

Tracking: #2920. 跨仓配对 PR:objectstack-ai/objectui#2501

管理员现在能用同一套词表分清权限集/岗位/能力是「平台发的 / 应用发的 / 自己建的」。三个 RBAC 目录对象的 managed_by 此前各说各话,本 PR 统一为 platform / package / admin(以 sys_capability 为准)。

改动

  • sys_permission_set.managed_by / sys_position.managed_bytext → 约束 select,选项 platform / package / admindefaultValue: 'admin'readonly —— 与 sys_capability 完全一致。
  • 写入端收敛到规范词表
    • 内置身份/受众锚点岗位(bootstrapBuiltinRoles):managed_by: 'system''platform'
    • 环境/Studio 自建权限集(upsertEnvPermissionSet 创建路径):'user''admin'
    • 声明式包权限集('package')与平台能力('platform')本就是规范值,未动。
  • sys_position 四个 listViewactive / default_positions / custom / all_positions)补上 managed_by 列,与 capability/permission-set 视图对齐;并加入 highlightFields
  • i18nmanaged_by 字段 + 选项标签(platform/package/admin),三对象 × 四语言(en / zh-CN / ja-JP / es-ES)补齐;顺带补 sys_capability 的 scope 选项标签。

历史数据兼容策略(无破坏性 migration)

关键取舍:没有一处运行时逻辑对分歧的旧值分支——所有访问决策只读 'package' / 'platform'(两者跨三对象本就一致、未改),因此这次是纯展示词表重命名,永不改变任何鉴权结果

  • 内置岗位、声明式包权限集在下次 bootstrap upsert 时自愈到规范值。
  • 残留旧值(环境自建集的 'user'、旧岗位的 system/config/user)由新增的幂等 kernel:ready 协调器 normalizeManagedByVocab 重写(system→platformconfig→packageuser→admin)。字段为 readonly,故以 isSystem 上下文写入。
  • 之所以选协调器而非一次性 migration 脚本:本 kernel 的既定约定就是「幂等 bootstrap 即 migration」(bootstrapBuiltinRoles / bootstrapDeclaredPermissions 等皆如此),与之保持一致。

验证

  • pnpm --filter @objectstack/plugin-security test —— 341 passed(16 files)
  • pnpm --filter @objectstack/plugin-security build —— ESM/CJS/DTS 均通过。
  • 新增 normalize-managed-by.test.ts(重写正确性 + 幂等 + 空 ql 容错);rbac-objects.test.ts 新增三对象词表一致性 + 岗位视图列断言;projection/anchor 测试的创建路径断言随写入端更新(useradminsystemplatform)。

存疑点

  • managed_bytextselectselect 只在写入时校验选项、读取不校验,故残留旧值行仅在协调器跑到前展示原始字符串,无硬失败。
  • 未改 security-plugin 的 package 伪造闸(仍只认 'package'),亦未改对象级 managedBy: 'config'(那是 schema 保护级别,与记录级 managed_by 字段无关)。

🤖 Generated with Claude Code

https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs

Unify the record-level provenance vocabulary across sys_capability,
sys_permission_set and sys_position onto platform / package / admin.
- sys_permission_set.managed_by and sys_position.managed_by: text -> select
(options platform/package/admin, defaultValue admin, readonly), matching
sys_capability.
- Writers re-stamped: built-in positions seed managed_by:'platform' (was
'system'); env/Studio-authored permission sets project managed_by:'admin'
(was 'user').
- sys_position list views (active/default_positions/custom/all_positions) now
surface the managed_by column.
- No destructive migration: no runtime path branches on the legacy values
(every access decision keys on 'package'/'platform'), so the rename never
changes an authorization outcome. Built-ins/declared sets self-heal on their
bootstrap upsert; new idempotent kernel:ready reconciler normalizeManagedByVocab
rewrites residual legacy rows (system->platform, config->package, user->admin).
- i18n: managed_by field + option labels added for all three objects across
en / zh-CN / ja-JP / es-ES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 15, 2026 12:15am

Request Review

@github-actions

github-actionsBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/getting-started/cli.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx(via packages/dogfood, packages/plugins/plugin-security)
  • content/docs/permissions/delegated-administration.mdx(via packages/dogfood)
  • 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-security)
  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-security)
  • 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.

…A4 vocab)
A4 unified record-level managed_by onto platform/package/admin; env-authored
permission sets now project as 'admin' (formerly 'user'). Update the two
showcase-permission-projection assertions the vocab change made stale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
claude added 2 commits July 14, 2026 23:50
…ed-by-tristate
# Conflicts:
#	packages/plugins/plugin-security/src/security-plugin.ts
@os-zhuang
os-zhuang marked this pull request as ready for review July 15, 2026 00:09
@os-zhuang
os-zhuang merged commit 5febe3f into mainJul 15, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/authz-a4-managed-by-tristate branch July 15, 2026 00:20
os-zhuang pushed a commit that referenced this pull request Jul 15, 2026
…ows (#2926 ①)
The system-row write gate (#2918/#2930) keyed sys_position provenance on the
legacy managed_by values (system/config), but the A4 vocab unification (#2934)
stamps and boot-normalizes rows to platform/package — so the gate silently
stopped firing for positions, letting admins physically delete the everyone/
guest audience anchors once their bindings were removed. Guard both the
canonical and legacy vocabularies, update the gate tests to the canonical
values, add legacy-row regressions, and correct the stale 'no runtime path
branches on legacy values' safety comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
os-zhuang added a commit that referenced this pull request Jul 15, 2026
)
* fix(plugin-security): protect platform/package-stamped sys_position rows (#2926 ①)
The system-row write gate (#2918/#2930) keyed sys_position provenance on the
legacy managed_by values (system/config), but the A4 vocab unification (#2934)
stamps and boot-normalizes rows to platform/package — so the gate silently
stopped firing for positions, letting admins physically delete the everyone/
guest audience anchors once their bindings were removed. Guard both the
canonical and legacy vocabularies, update the gate tests to the canonical
values, add legacy-row regressions, and correct the stale 'no runtime path
branches on legacy values' safety comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* fix(rest): pass through explicit 4xx status+code in mapDataError (#2926 ⑦)
Record-scope authorization denials (plugin-sharing throws status=403,
code=FORBIDDEN) degraded to a bare 400 with no code because the generic data
routes call mapDataError directly, bypassing sendError's status passthrough.
Add a guarded 4xx passthrough after the structured-code branches (409
envelopes keep their rich fields; 5xx still goes through the sanitizing
heuristics; oversized messages fall back to generic text), and stop logging
expected statuses as unhandled on the list route.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* feat(metadata-protocol): reject unknown $-prefixed query params with 400 (#2926 ⑩)
Unsupported $ parameters (e.g. $foo, $inlinecount) used to fall into the
implicit-filter bucket and silently match zero rows — and before the $filter
alias existed, were dropped entirely, returning the unfiltered first page to
callers that believed they had a filtered result set. All supported aliases
are consumed before the guard, so anything $-prefixed that remains is a hard
400 UNSUPPORTED_QUERY_PARAM naming the offending keys and the supported list.
Bare-key implicit equality filters are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* feat(plugin-sharing): backfill rule grants at boot (#2926 ③)
Sharing-rule grants are materialized by write hooks that deliberately skip
isSystem writes, so seed-loader records never got sys_record_share rows —
demo data with matching rules was broken on a fresh deploy until each record
was touched at runtime. Reconcile every active rule once per boot (idempotent,
best-effort per rule) right after the rule hooks bind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* feat(plugin-sharing): sys_sharing_rule provenance + seed-not-clobber (#2909 P0/T1)
sys_sharing_rule is record-authoritative (ADR-0094 addendum): declared rules
are a boot seed, the row is the authority — but every boot re-ran a clobbering
upsert, so an admin's active:false on an over-sharing rule was silently
resurrected on redeploy. Add readonly managed_by (A4 tri-state) + customized
provenance columns, put defineRule in seed mode when the bootstrap passes
managedBy:'package' (pristine/legacy rows adopted and updated; admin-authored
or customized rows untouched), and stamp customized via a beforeUpdate hook on
any non-system edit of a seeded rule. No write gate on purpose — sharing rules
stay a first-class admin authoring surface; edits are remembered, not blocked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* test(plugin-security): lock bootstrap-declared-positions seed semantics (#2909 T2)
sys_position is record-authoritative — the declared seeder refreshes only
label/description and must never touch bindings, active, is_default,
delegatable, or managed_by. That behavior predates these tests but was never
locked; regressions would silently clobber admin state at every boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* fix(plugin-security): stop clobbering admin-edited capability scope (#2909 T3)
scope is an admin-editable classification select on sys_capability, but the
curated seeder refreshed it every boot — silently reverting admin
reclassifications. Make it seed-once (insert only); label/description remain
platform-owned and keep refreshing. Lock both sides with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* docs(adr): resolve 0094 addendum audit items; record sharing-rule seed-not-clobber tradeoff (#2909 T4)
The three 'Audit that…' per-type rows are now shipped decisions:
sys_sharing_rule = record-authoritative with provenance + seed-not-clobber;
sys_position = seed-only identity/display, locked by test; sys_capability =
seed-not-clobber with scope seed-once. Record why sharing rules got no write
gate (admin tuning surface), the customized-stamp boundaries, and the scope
migration cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* feat(app-showcase): bind persona positions on kernel:listening + announcements; document isDefault dual-track (#2926 ②⑤)
A fresh deploy booted with ZERO position↔permission-set bindings — every
persona silently degraded to the everyone baseline. Bindings are
record-authoritative (ADR-0090/0094) and cannot be a declarative seed (the
seed loader runs before the security bootstrap creates sys_position /
sys_permission_set, so name references can't resolve). They are ensured
imperatively by registerShowcasePositionBindings on kernel:listening — the
phase that fires only after every kernel:ready handler (incl. the security
bootstrap) has settled, so the referenced rows exist. everyone→member_default
is bound here too: the framework only auto-binds an app's isDefault set to
everyone when it is application-owned; the showcase ships as a package, so its
default lands in sys_audience_binding_suggestion (pending admin confirmation)
and is not live until confirmed.
Also seeds showcase_announcement demo rows and documents the isDefault
dual-track (app-level auto-bind vs package-level suggestion) in the spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
* fix(plugin-sharing): defer rule backfill to kernel:listening so seed rows materialize (#2926 ③)
The boot backfill ran inside a kernel:ready handler, but SeedLoader also
seeds on kernel:ready (raced against a budget, in a different AppPlugin
handler). Since kernel:ready handlers fire sequentially in registration
order, the backfill could run before the seed records exist and materialize
nothing. Move the reconcile to kernel:listening (Phase 4), which the kernel
fires only after every kernel:ready handler has settled — so the seeded rows
are present. Verified end-to-end: a seeded red project now yields a
sys_record_share to the exec recipient at boot, no runtime touch required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
---------
Co-authored-by: Claude <noreply@anthropic.com>
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