Skip to content

feat(auth): 身份表写守卫 — managedBy:'better-auth' 引擎级强制(ADR-0092 D2/D3/D6,closes #2816) - #2828

Merged
os-zhuang merged 2 commits into
mainfrom
claude/sys-user-field-delegation-4p8l20
Jul 11, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/sys-user-field-delegation-4p8l20

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

概要

ADR-0092 实现 A(#2816):managedBy: 'better-auth' 从"文档承诺"变为引擎真实边界。

  • identity-write-guard.ts — plugin-auth 在 kernel:ready 注册 before{Insert,Update,Delete} hook(priority 10,先于审计快照)。判定按 schema registry 的 managedBy === 'better-auth' 标志(无硬编码表清单);用户上下文写入 default-deny,403 PERMISSION_DENIED 且错误信息指路专用端点。better-auth adapter(无 context)与 isSystem 写入直通。
  • 白名单 registry — registerManagedUpdateWhitelist(object, fields) 是唯一开口;首条目 sys_user → {name, image}。名单外字段剥离;剥空抛错(响亮失败)。引擎盖章的生命周期列(updated_at/updated_by)直通但不计入白名单判定 —— 否则纯 email PATCH 会退化成时间戳触碰而非报错(dogfood 中发现并修复)。
  • sys-user-writable-fields.ts(D3)— 共享字段分层:档案白名单 {name, image} 与 import 超集 {+phone_number, role}(spread 派生防漂移);admin-import-users.ts 的 UPDATE_ALLOWED_FIELDS 改为共享常量。
  • D6 — afterUpdate hook 镜像 better-auth refreshUserSessions:守卫放行的档案编辑后,按原 TTL 重写(绝不删除,避免误登出)secondary storage 中该用户的 {session, user} 快照。

验证

单测(新增 15 项,plugin-auth 全套 387 项通过):逐表用户上下文 insert/delete/update 拒绝、白名单剥离与空包抛错、system/无 context 直通、multi-update 过滤、生命周期列直通、D6 快照重写/无 storage no-op/storage 故障不破坏写入、字段分层超集关系。

真机验证(--fresh CRM 后端 + seeded platform admin,curl):

操作 结果
PATCH sys_user name ✅ 200,持久化,updated_at 刷新
PATCH 仅 email ✅ 403 + "Editable fields: name, image" 指路
PATCH name+role+must_change_password ✅ 200,role/mcp 剥离未落库
POST / DELETE sys_user ✅ 403
/admin/create-user(临时密码 + mcp 盖章) ✅ 正常
/admin/set-user-password ✅ 正常
自助 /update-user ✅ 正常(better-auth 路径)
/admin/import-users upsert 更新 ✅ 正常(SYSTEM_CTX 直通)

注:/admin/ban-user 返回 better-auth 原生 YOU_ARE_NOT_ALLOWED_TO_BAN_USERS(stock 插件只认 role 标量,seeded admin role=user)—— 既有行为,与本守卫无关(错误码可区分)。

调用点扫描:全仓身份表写调用点(plugin-auth × 9、plugin-sharing × 2)全部已是 SYSTEM_CTX,无需迁移。

迁移说明(见 changeset)

以用户上下文直写身份表的服务端脚本需改 system context(真内部写)或走专用端点;flow/automation 以用户身份写非档案 sys_user 列的同样被过滤。

相关

ADR-0092(Accepted)· closes #2816 · 后续:#2817(affordance 翻转,依赖本 PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21


Generated by Claude Code

… the engine (#2816)

ADR-0092 D2/D3/D6 implementation:

- identity-write-guard.ts: before{Insert,Update,Delete} hooks (priority 10)
  registered by plugin-auth at kernel:ready. User-context writes to any
  object whose schema declares managedBy:'better-auth' are rejected
  fail-closed (403 PERMISSION_DENIED with a pointer to the dedicated
  surfaces); better-auth adapter (no context) and isSystem writes bypass.
  Per-object update whitelist registry is the only opening; engine-stamped
  lifecycle columns (updated_at/updated_by) pass through but never satisfy
  the whitelist alone.
- sys-user-writable-fields.ts: shared field tiers — profile edit whitelist
  {name, image} and the import superset {+phone_number, role}
  (subset-by-construction, ADR-0092 D3); admin-import-users.ts now derives
  UPDATE_ALLOWED_FIELDS from it.
- D6: afterUpdate hook mirrors better-auth's refreshUserSessions — cached
  {session, user} snapshots in secondary storage are re-written (same TTL,
  never deleted) with the changed profile fields after a guarded edit.

Verified against a live --fresh CRM backend: profile PATCH persists with
fresh updated_at; email-only PATCH 403s; role/must_change_password are
stripped and not persisted; insert/delete 403; create-user /
set-user-password / self-service update-user / import upsert all work
unchanged. Call-site sweep: every existing identity-table write already
runs under SYSTEM_CTX or the adapter path.

Closes #2816

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
@vercel

vercel Bot commented Jul 11, 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 11, 2026 3:53am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 11, 2026
@github-actions

github-actions Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx (via packages/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

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.

The old test asserted POST /data/sys_team returns 201 — a raw data-API
path the product UI never exposes (sys_team's own schema gates every
team-mutation affordance to multi-org mode, and its actions name the
better-auth team endpoints as the canonical entry points). The identity
write guard now rejects that user-context insert by design.

The ADR-0057 regression target — organization_id optional at the schema
level so single-tenant writes don't VALIDATION_FAIL — is kept, asserted
through the writers that remain legitimate post-ADR-0092: system-context
engine writes. The user-context 403 (PERMISSION_DENIED) becomes explicit
coverage of the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
@os-zhuang
os-zhuang marked this pull request as ready for review July 11, 2026 07:30
@os-zhuang
os-zhuang merged commit a348394 into main Jul 11, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/sys-user-field-delegation-4p8l20 branch July 11, 2026 07:30
os-zhuang pushed a commit that referenced this pull request Jul 11, 2026
… fields (ADR-0092 D4)

sys_user gains userActions:{edit:true} (create/import/delete stay off).
name/image are editable; email, email_verified, two_factor_enabled, role,
banned/ban_reason/ban_expires, ai_access, manager_id,
primary_business_unit_id and every system-managed stamp are marked
readonly so the standard edit form renders them non-editable.

Safe because the server boundary is the identity write guard (ADR-0092
D2, #2828): a user-context sys_user update may only touch {name, image}
regardless of what a form submits. The readonly flags are UX only.

Verified in the running Console: the object metadata sent to the client
carries userActions.edit=true and the correct per-field readonly flags;
the row 'Edit' action appears; the edit form surfaces only name/image;
and — the risk this issue flagged — the create_user/invite_user dialogs
keep email/role editable (action params do not inherit field readonly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
os-zhuang pushed a commit that referenced this pull request Jul 11, 2026
…ect FLS + bump objectui to sys_user edit-form fix (ADR-0092 D4)

Completes ADR-0092 D4 end-to-end (#2817). Three parts brought the standard
sys_user edit form to life for platform admins, verified in a running
Console (edit name → persisted; email/role stay read-only):

1. foldWildcardSuperUser in /me/permissions (hono-plugin): the per-object
   FLS map now folds the '*' modifyAll/viewAll super-user grant into every
   object entry, so the client mirrors the server's actual enforcement
   (checkObjectPermission is most-permissive, no deny-wins). Fixes the
   false-negative where a platform admin who also holds organization_admin
   (denies identity writes) saw sys_user.allowEdit:false and a disabled
   form, though the server accepts the write (PATCH → 200). ADR-0057 D10.
2. .objectui-sha bump to 5da9905 — pulls in objectui#2395 (ObjectForm
   honors userActions.edit instead of blanket-disabling managed objects).
3. sys_user userActions.edit + field readonly governance (earlier commit).

The identity write guard (#2828) still restricts the actual write to
{name, image}; these changes only correct which affordances the UI shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
os-zhuang pushed a commit that referenced this pull request Jul 11, 2026
…ission ∩ guard (ADR-0092 complete fix)

Completes the /me/permissions consistency fix. foldWildcardSuperUser
alone fixed the false-NEGATIVE (admin couldn't see sys_user editable)
but introduced a false-POSITIVE: it reported allowEdit:true for every
better-auth identity table, including ones the identity write guard
(#2828) actually blocks (sys_member, sys_account, sys_session, …).

clampManagedObjectWrites re-applies the guard's user-context policy: a
managedBy:'better-auth' object is writable only where it opted in via
userActions.{edit,create,delete} (sys_user → edit; the field readonly
flags then narrow it to {name,image}). Only better-auth objects are
clamped — system/config/append-only have no such guard, so their
permission-set result stands (admin CAN write them; must not under-report).

Net: the Console's per-object FLS now equals real server enforcement
(permission-set ∩ guard). Verified against a running stack: sys_user
allowEdit:true + form editable; sys_member/session/account allowEdit:false
(read preserved). +4 unit tests (8 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
os-zhuang added a commit that referenced this pull request Jul 11, 2026
…-0092 D4) (#2832)

Completes ADR-0092 D4 (closes #2817), verified end-to-end in a running
Console (edit name → persisted; email/role read-only):

- sys_user: userActions.edit + per-field readonly governance (name/image
  editable, everything else read-only).
- /me/permissions now reflects permission-set ∩ identity-write-guard:
  foldWildcardSuperUser fixes the false-negative (admin's '*' modifyAll
  shadowed by organization_admin's explicit identity deny), and
  clampManagedObjectWrites re-applies the guard's user-context policy so
  managed tables the guard blocks (sys_member/session/account) stay
  non-editable while sys_user (opted in) is editable. Fixes #2836.
- .objectui-sha bump pulls in objectui#2395 (ObjectForm honors
  userActions.edit instead of blanket-disabling managed objects).

The identity write guard (#2828) still restricts the actual write to
{name, image}; the UI now shows exactly what the server enforces.

This branch was successfully deployed

1 active deployment
Preview — 7298e732 Deployed Jul 11, 2026 by vercel[bot]
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.

feat(identity): ADR-0092 实现 A — 通用身份表写守卫(registry 驱动 default-deny + sys_user 档案白名单)

2 participants