feat(auth): 身份表写守卫 — managedBy:'better-auth' 引擎级强制(ADR-0092 D2/D3/D6,closes #2816)#2828
Merged
Merged
Conversation
… 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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
marked this pull request as ready for review
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
This was referenced Jul 11, 2026
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
ADR-0092 实现 A(#2816):
managedBy: 'better-auth'从"文档承诺"变为引擎真实边界。kernel:ready注册before{Insert,Update,Delete}hook(priority 10,先于审计快照)。判定按 schema registry 的managedBy === 'better-auth'标志(无硬编码表清单);用户上下文写入 default-deny,403PERMISSION_DENIED且错误信息指路专用端点。better-auth adapter(无 context)与isSystem写入直通。registerManagedUpdateWhitelist(object, fields)是唯一开口;首条目sys_user → {name, image}。名单外字段剥离;剥空抛错(响亮失败)。引擎盖章的生命周期列(updated_at/updated_by)直通但不计入白名单判定 —— 否则纯 email PATCH 会退化成时间戳触碰而非报错(dogfood 中发现并修复)。{name, image}与 import 超集{+phone_number, role}(spread 派生防漂移);admin-import-users.ts的UPDATE_ALLOWED_FIELDS改为共享常量。afterUpdatehook 镜像 better-authrefreshUserSessions:守卫放行的档案编辑后,按原 TTL 重写(绝不删除,避免误登出)secondary storage 中该用户的{session, user}快照。验证
单测(新增 15 项,plugin-auth 全套 387 项通过):逐表用户上下文 insert/delete/update 拒绝、白名单剥离与空包抛错、system/无 context 直通、multi-update 过滤、生命周期列直通、D6 快照重写/无 storage no-op/storage 故障不破坏写入、字段分层超集关系。
真机验证(
--freshCRM 后端 + seeded platform admin,curl):sys_usernameupdated_at刷新sys_user/admin/create-user(临时密码 + mcp 盖章)/admin/set-user-password/update-user/admin/import-usersupsert 更新注:
/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