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-zhuang os-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

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 15, 2026 12:15am

Request Review

@github-actions

github-actions Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants