Skip to content

fix(security): derive better-auth managed-object write denies from the registry (#3325) - #3326

Merged
os-zhuang merged 1 commit into
mainfrom
claude/managedby-taxonomy-split-yder9o
Jul 20, 2026
Merged

fix(security): derive better-auth managed-object write denies from the registry (#3325)#3326
os-zhuang merged 1 commit into
mainfrom
claude/managedby-taxonomy-split-yder9o

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#3325. Follow-through of #3220 / ADR-0103 (merged in #3315).

Problem

The default permission sets deny generic writes on better-auth identity tables via a hand-maintained BETTER_AUTH_MANAGED_OBJECTS list — the drift ADR-0092 forbids, and it had already drifted: 17 listed vs 22 schemas declaring managedBy: 'better-auth', leaving sys_scim_provider, sys_sso_provider, sys_oauth_resource, sys_oauth_client_resource, sys_oauth_client_assertion wildcard-granted for writes at the permission-evaluator layer. Not exploitable (plugin-auth's identity write guard still 403s the write), but a defense-in-depth gap + an ADR-0049 unenforced-property.

Approach

Derive the deny-list from the live registry so the permission layer can't disagree with the schemas. Two facts shaped it (both pinned by tests):

  1. A DB-row-only fix would be dead code — the evaluator resolves the default sets from the in-memory bootstrapPermissionSets, not the seeded DB JSON. The transform mutates those shared PermissionSet instances in place at kernel:ready, before the seeder, so every consumer (evaluator, manifest.register, seed row) sees it atomically.
  2. sys_user is an intentional divergence — it declares userActions: { edit: true } (a field-level whitelist the identity guard enforces), so the deny must key off the raw better-auth bucket and hard-deny writes, not derive from resolveCrudAffordances (which would widen sys_user edit).

Changes

  • New internal applyManagedWriteDenies (managed-object-write-denies.ts): injects a read-only-write deny for every registered managedBy: 'better-auth' object into the four write-granting sets (organization_admin, member_default, viewer_readonly, MCP write). Never touches admin_full_access, never overrides an existing entry, idempotent.
  • Wired into security-plugin.tsrunBootstrap (once-flag, before bootstrapPlatformAdmin), reading the registry via the existing readDeclared(ql, 'object').
  • Static BETTER_AUTH_MANAGED_OBJECTS completed to 22 and kept as a compile-time baseline (covers the pre-kernel:ready window), now pinned bidirectionally against the @objectstack/platform-objects schemas.

Explicitly out of scope

Engine-owned system/append-only objects are not given deny entries — a per-object entry overrides the wildcard and would drop viewAllRecords; their writes are already rejected by the ADR-0103 engine guard (assertEngineOwnedWriteAllowed) and reflected in the /me/permissions clamp. Documented in the module header.

Verification

@objectstack/plugin-security539 passed (was 521; +18), build/DTS typecheck clean. New/extended tests: helper behavior, the drift pin (fails on the old 17-name list — proof it bites), seed-row serialization + resync, and a reference-sharing integration pin (a registry-only better-auth object lands on the shared default set).

Accepted side effects (byte-preserving for the 17 already-listed tables)

  • organization_admin loses wildcard viewAllRecords/modifyAllRecords on the 5 newly-denied objects — identical posture to the existing 17 (allowRead retained).
  • Writes to the 5 tables under member_default now fail at the evaluator instead of the engine guard — same outcome, different error origin.

Related

#3220 · #3315 · ADR-0092 (registry-driven, no hardcoded lists) · ADR-0103 · ADR-0049

🤖 Generated with Claude Code


Generated by Claude Code

…e registry (#3325)
The default permission sets deny generic writes on better-auth identity tables
via a hand-maintained BETTER_AUTH_MANAGED_OBJECTS list — the drift ADR-0092
forbids, and it had already drifted: 17 listed vs 22 schemas declaring
managedBy:'better-auth', leaving sys_scim_provider, sys_sso_provider and three
sys_oauth_* tables wildcard-granted for writes at the evaluator (the identity
write guard still 403'd the write, so a defense-in-depth gap, not a live hole).
- New applyManagedWriteDenies (managed-object-write-denies.ts) unions the denies
from the live registry into the four write-granting default sets at
kernel:ready, mutating the shared in-memory bootstrapPermissionSets in place
(the array the evaluator resolves and the seeder serializes — a DB-row-only fix
would be dead code). Never touches admin_full_access, never overrides an
existing entry, ignores userActions (better-auth is hard-denied: sys_user's
userActions.edit opens only a field whitelist the identity guard enforces).
- Static list completed to 22 as a compile-time baseline, now pinned
bidirectionally against the @objectstack/platform-objects schemas by test so it
cannot silently rot again.
- Engine-owned system/append-only objects deliberately NOT denied here (a
per-object entry overrides the wildcard and would drop viewAllRecords; their
writes are already rejected by the ADR-0103 engine guard).
Tests: helper behavior, drift pin, seed-row serialization + resync, and a
reference-sharing integration pin. plugin-security 539 passed.
Refs #3325, follows #3220 / ADR-0092 / ADR-0103 / ADR-0049.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security.

11 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 @objectstack/plugin-security)
  • 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.

@vercel

vercelBot commented Jul 20, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 20, 2026 1:40am

Request Review

@os-zhuang
os-zhuang marked this pull request as ready for review July 20, 2026 01:50
@os-zhuang
os-zhuang merged commit 9d897b3 into mainJul 20, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/managedby-taxonomy-split-yder9o branch July 20, 2026 01:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-zhuang@claude