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.ts runBootstrap (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-actions github-actions Bot 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

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

Labels

Projects

None yet

2 participants