Skip to content

fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220)#3222

Merged
os-zhuang merged 1 commit into
mainfrom
claude/continue-task-1591-yqg8hb
Jul 18, 2026
Merged

fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220)#3222
os-zhuang merged 1 commit into
mainfrom
claude/continue-task-1591-yqg8hb

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Addresses the concrete, safe slice of #3220 (the follow-through on #1591/#3213). A blast-radius audit determined the broad engine-level write guard over system/append-only is not worth building — near-zero real security gain, high regression risk to delegated administration — so this PR ships only the two genuine latent holes the audit confirmed.

What & why

Two non-better-auth managed objects shipped the exact contradiction #3213 fixed for better-auth objects: enable.apiMethods advertised generic create/update/delete while their managedBy bucket forbids user-context writes — leaving the generic /data route open for a write the bucket does not permit.

  • sys_presence (managedBy: 'append-only') advertised create/update/deleteupdate/delete on an append-only object — but is written only over the realtime websocket/in-memory path, never through ObjectQL. → ['get', 'list'].
  • sys_metadata (managedBy: 'system') advertised full CRUD, but customization overlays are authored only through the metadata-protocol RPC (its engine writes carry a transaction context, not a user session). Confirmed neither the framework nor the Console (objectui) POSTs /data/sys_metadata. → ['get', 'list'].

Reads stay open. The metadata-protocol / realtime write paths are engine-level and bypass the HTTP exposure gate, so they are unaffected.

Why not the broader guard

The audit found the system bucket is overloaded: several system objects are user-writable by designsys_user_position, sys_user_permission_set, sys_position_permission_set (the whole DelegatedAdminGate is built on non-isSystem user writes), plus sys_user_preference and sys_import_job. A blanket affordance-driven engine guard would break delegated administration for near-zero gain (the genuinely engine-only tables already write via isSystem, and the key audit tables — sys_audit_log/sys_activity — are already ['get','list']). The real fix is a bucket-taxonomy split, tracked as the root cause in #3220.

Verification

  • service-realtime sys-presence object suite (10 passed) — new assertion pins apiMethods === ['get','list'] and rejects re-introducing a write verb.
  • metadata-core (100 passed); objectql overlay engine-insert tests (6 passed) — confirm sys_metadata engine writes still work (they bypass the HTTP gate).
  • Metadata-authoring dogfood package-first-authoring (5 passed) — end-to-end customization authoring unaffected.

Refs #3220. Follows #3213 / ADR-0049 / ADR-0092.

🤖 Generated with Claude Code


Generated by Claude Code

…sys_metadata (#3220)

Follow-through on #1591/#3213 for two non-better-auth managed objects that
shipped the same contradiction the better-auth reconciliation fixed: their
enable.apiMethods advertised generic create/update/delete while their managedBy
bucket forbids user-context writes, leaving the generic /data route open.

- sys_presence (append-only) advertised create/update/delete — update/delete on
  an append-only object — but is written only over the realtime websocket/
  in-memory path, never through ObjectQL. Narrowed to ['get','list'].
- sys_metadata (system) advertised full CRUD but overlays are authored only via
  the metadata-protocol RPC (engine writes carry a transaction context, not a
  user session); neither the framework nor the Console (objectui) POSTs
  /data/sys_metadata. Narrowed to ['get','list'].

Reads stay open. The metadata-protocol / realtime write paths are engine-level
and bypass the HTTP exposure gate, so they are unaffected — verified by the
metadata-authoring dogfood (5 passed), the objectql overlay engine-insert tests
(6 passed), and metadata-core (100 passed).

A blast-radius audit found the broader system/append-only buckets are NOT safe
to guard wholesale: several system objects (sys_user_position,
sys_user_permission_set, sys_position_permission_set, sys_user_preference,
sys_import_job) are user-writable by design (delegated administration, user
preferences, imports). Generalizing the engine write guard to those buckets is
intentionally out of scope — the root cause is the overloaded system bucket,
tracked in #3220.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE
@vercel

vercel Bot commented Jul 18, 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 18, 2026 2:42pm

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, packages/services.

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

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-core)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/plugins/packages.mdx (via packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)
  • content/docs/releases/v12.mdx (via @objectstack/metadata-core)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 15:00
@os-zhuang
os-zhuang merged commit 06cb319 into main Jul 18, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/continue-task-1591-yqg8hb branch July 18, 2026 15:00
os-zhuang added a commit that referenced this pull request Jul 19, 2026
…ritable, guard engine-owned writes (#3220) (#3315)

The `system` bucket conflated two incompatible write policies — engine-owned
rows (never user-written) and platform-schema, admin/user-writable data — under
one all-false affordance row with no engine enforcement (only better-auth had a
write guard). This left a wildcard admin able to raw-write engine-owned rows
through the generic data API (ADR-0049 gap).

Rather than add a new managedBy enum value (which falls through to fully-editable
platform defaults on deployed Console clients), the write policy is now the
resolved affordance (resolveCrudAffordances = bucket default + userActions), and
"engine-owned" is defined as a system/append-only object granting no write:

- Writable set declares userActions{create,edit,delete}: the RBAC link tables,
  sys_user_preference, sys_approval_delegation, and the messaging config grids
  (notification preference/subscription/template). Affordance only — the
  DelegatedAdminGate / RLS / permission sets remain the authz.
- Engine-owned objects locked to apiMethods ['get','list'] where absent (jobs,
  notifications, approvals, record-share, automation-run, mail/settings/secret
  audit, messaging delivery pipeline). sys_secret is read-locked explicitly (an
  empty apiMethods array fails open).
- sys_import_job stays engine-owned: the REST import route writes job rows
  isSystem-elevated (attribution preserved via explicit created_by).
- New engine write guard (assertEngineOwnedWriteAllowed, plugin-security)
  fail-closed rejects user-context generic writes keyed off resolved
  affordances; isSystem/context-less writes bypass. Wired into the security
  middleware alongside the other data-layer gates.
- reconcileManagedApiMethods (objectql registry) now runs for every managed
  bucket, stripping advertised write verbs the affordances forbid.
- /me/permissions clamp (plugin-hono-server) now clamps system/append-only too.

ADR-0103. Refs #3220, follows #3222 / ADR-0049 / ADR-0092.


Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE

Co-authored-by: Claude <noreply@anthropic.com>
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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants