Repository navigation
Description
Rewritten 2026-07-18 after a blast-radius audit. The original proposal here was to generalize the ADR-0092 engine write guard +
apiMethodsreconciliation frombetter-authto thesystem/append-onlybuckets. The audit disproved that as a wholesale change and narrowed the real, safe work to two latent holes (shipped in #3222). This issue now tracks the actual root cause it surfaced: thesystembucket conflates two incompatible write policies.
What the audit found
For the better-auth case (#1591/#3213) the guard was clearly right: admin_full_access (wildcard, no RLS) could raw-write identity columns via the generic API. The premise for extending it — "system/append-only also mean no user writes, just enforce it" — turned out to be false in this codebase.
1. The genuinely engine-only tables are already safe. sys_audit_log, sys_activity, sys_automation_run, approval/share/notification/job/settings/secret/metadata-history are all written via isSystem: true or a service engine with no user session (audit-writers.ts:385; approval-service.ts SYSTEM_CTX; messaging/jobs service engines). A user-context guard would reject nothing they do today — pure speculative defense-in-depth. And the key audit tables already expose ['get','list'], so they're locked at the HTTP layer already.
2. append-only immutability already effectively holds. No append-only object receives a user-context UPDATE/DELETE. The only append-only UPDATE is sys_email status, under SYSTEM_CTX.
3. system is overloaded — the real bug. Several system-bucketed objects are user-writable by design, so a blanket affordance-driven guard would break them:
| Object | Why it takes real user-context writes |
|---|---|
sys_user_position |
delegated admin "add position" — delegation-of-duty.dogfood.test.ts:83 POSTs under a user token; DelegatedAdminGate governs it |
sys_user_permission_set |
delegated manageBindings direct grants — showcase-permission-zoo.dogfood.test.ts:175 |
sys_position_permission_set |
suggested-audience-bindings.ts:371 writes with context: callerCtx (explicitly not isSystem) |
sys_user_preference |
user authors their own prefs (RLS self-grant default-permission-sets.ts:213) |
sys_import_job |
REST import route writes under the user session (rest-server.ts:3727) |
These conflate with the engine-only rows under one bucket. That is the defect: managedBy: 'system' means both "engine-owned, never user-written" and "platform-defined schema, admin-writable data". No single write policy is correct for the bucket.
What shipped instead (the safe slice) — #3222
Only the two objects with a genuine latent hole (write verbs their bucket forbids and no legitimate user-write path):
sys_presence(append-only, even listedupdate/delete) →['get','list'](written over the realtime path, never ObjectQL).sys_metadata(system) →['get','list'](authored via metadata-protocol RPC; confirmed no/data/sys_metadatawrite in framework or objectui).
The remaining work this issue now tracks
Split the system taxonomy so write policy is expressible and enforceable without breaking admin-writable data:
- Introduce a distinction between engine-owned rows (no user writes — e.g.
sys_automation_run,sys_job,sys_notification) and platform-schema / admin-writable data (e.g. the RBAC/delegation trio,sys_user_preference,sys_import_job). Options: a new bucket value, or an explicituserActionsdeclaration on the writable ones soresolveCrudAffordancesreflects reality. - Only after the taxonomy is correct, reconsider an engine-level guard for the genuinely engine-owned set (defense-in-depth for non-REST callers) — driven off the corrected affordances, with
isSystembypass. - Fix the remaining
apiMethodscontradictions on the writable objects by declaring their true policy (not by stripping verbs they legitimately need).
This is an ADR-scale taxonomy decision (touches delegated administration), not a mechanical guard extension — hence tracked here rather than folded into #3222.
Explicitly out of scope
The writeVia: '<endpoint>' capability for third-party-managed objects — no such object exists today; speculative until a concrete need arises.
Related
- managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591 / fix(identity): reconcile managedBy:'better-auth' apiMethods with the write guard (#1591) #3213 (better-auth half — landed) · fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220) #3222 (the safe slice — sys_presence/sys_metadata)
- ADR-0049 (enforce-or-remove), ADR-0092 (identity write guard mechanism)
Rewritten by Claude Code
Activity
[-]Generalize the managed-write guard + apiMethods reconciliation to `system` / `append-only` buckets (ADR-0049 / ADR-0092 follow-through)[/-][+]The `managedBy: 'system'` bucket is overloaded (engine-only vs. admin-writable) — split the taxonomy; don't guard it wholesale[/+]fix(identity): close generic-write apiMethods hole on sys_presence & …
feat(security): split managedBy:'system' into engine-owned vs admin-w…
os-zhuang commentedon Jul 20, 2026
Closing as completed — all three tracked items landed in #3315 (merged):
userActionson thesystem/append-onlywritable set (RBAC link tables,sys_user_preference,sys_approval_delegation, and the messaging config grids), withresolveCrudAffordancesas the single write-policy contract. No new enum value (avoids the fully-editable fallthrough on deployed clients).assertEngineOwnedWriteAllowed(plugin-security) fail-closes user-context generic writes to engine-owned objects,isSystem/context-less bypass;reconcileManagedApiMethodsgeneralized to every managed bucket;/me/permissionsclamp generalized.apiMethodscontradictions — declared the true policy on each object (['get','list']on the engine-owned set,userActionson the writable set;sys_import_jobroute elevated toisSystem).writeViafor third-party-managed objects stays out of scope (deferred, #1878) as noted.A latent drift the audit surfaced adjacent to this — the hand-maintained
BETTER_AUTH_MANAGED_OBJECTSdeny-list in the default permission sets (17 listed vs 22 schemas) — is tracked separately in #3325 and fixed in #3326, since it's a distinct ADR-0092 registry-driven concern rather than part of this taxonomy split.Generated by Claude Code
fix(security): derive better-auth managed-object write denies from th…
docs(data-modeling): document the managedBy lifecycle bucket and ADR-…