From 5adf5ce8e0bc8ebb560e6d70de18af8ed770f25c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 14:38:55 +0000 Subject: [PATCH] fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE --- ...econcile-system-append-only-api-methods.md | 33 +++++++++++++++++++ .../src/objects/sys-metadata.object.ts | 8 ++++- .../src/objects/sys-presence.object.test.ts | 8 +++++ .../src/objects/sys-presence.object.ts | 8 ++++- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .changeset/reconcile-system-append-only-api-methods.md diff --git a/.changeset/reconcile-system-append-only-api-methods.md b/.changeset/reconcile-system-append-only-api-methods.md new file mode 100644 index 0000000000..3f664ef217 --- /dev/null +++ b/.changeset/reconcile-system-append-only-api-methods.md @@ -0,0 +1,33 @@ +--- +"@objectstack/service-realtime": patch +"@objectstack/metadata-core": patch +--- + +fix(identity): close the generic-write apiMethods hole on sys_presence and sys_metadata (#3220) + +Follow-through on #1591/#3213 (better-auth apiMethods reconciliation) for two +non-better-auth managed objects that shipped the same contradiction: their +`enable.apiMethods` advertised generic `create`/`update`/`delete` while their +`managedBy` bucket forbids user-context writes, leaving the generic `/data` +route open to a write the bucket does not permit. + +- `sys_presence` (`managedBy: 'append-only'`) advertised `create`/`update`/`delete` + (update/delete on an append-only object at that) but is written only over the + realtime websocket/in-memory path, never through ObjectQL. Narrowed to + `['get', 'list']`. +- `sys_metadata` (`managedBy: 'system'`) advertised full CRUD but customization + overlays are authored only through 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 and the objectql overlay tests. + +A blast-radius audit confirmed 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 legitimately user-writable by design (delegated +administration, user preferences, imports). Generalizing the engine write guard +to those buckets is intentionally NOT done here — see #3220 for the bucket-taxonomy +root cause. diff --git a/packages/metadata-core/src/objects/sys-metadata.object.ts b/packages/metadata-core/src/objects/sys-metadata.object.ts index f24e88cb60..144274ab8a 100644 --- a/packages/metadata-core/src/objects/sys-metadata.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata.object.ts @@ -227,7 +227,13 @@ export const SysMetadataObject = ObjectSchema.create({ trackHistory: true, searchable: false, apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], + // #3220 — sys_metadata is `managedBy: 'system'`: customization overlays are + // authored ONLY through the metadata-protocol RPC (its engine writes carry a + // transaction context, not a user session), never the generic /data route. + // Neither the framework nor the Console (objectui) POSTs /data/sys_metadata, + // so the generic write verbs were a latent hole for a user-context raw write + // the bucket forbids. Reads stay open for the Setup "Data Model" grids. + apiMethods: ['get', 'list'], trash: false, }, diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts index 029327f745..9f3e84e1b0 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts @@ -70,4 +70,12 @@ describe('SysPresence object definition', () => { it('should have API enabled', () => { expect(SysPresence.enable?.apiEnabled).toBe(true); }); + + it('exposes reads only — append-only, written over the realtime path, never /data (#3220)', () => { + expect(SysPresence.enable?.apiMethods).toEqual(['get', 'list']); + // Guard against re-introducing a generic write verb on an append-only object. + for (const verb of ['create', 'update', 'delete', 'upsert']) { + expect(SysPresence.enable?.apiMethods).not.toContain(verb); + } + }); }); diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.ts b/packages/services/service-realtime/src/objects/sys-presence.object.ts index 26b1c458f8..562961a00f 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.ts @@ -113,7 +113,13 @@ export const SysPresence = ObjectSchema.create({ trackHistory: false, searchable: false, apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], + // #3220 — sys_presence is `managedBy: 'append-only'` and is written only + // over the realtime (websocket/in-memory) path, never through ObjectQL. + // Advertising create/update/delete here (and update/delete on an + // append-only object at that) was a latent hole: it left the generic + // /data route open for a user-context write the bucket forbids. Reads + // stay open for diagnostic list views. + apiMethods: ['get', 'list'], trash: false, mru: false, },