Skip to content

Commit 06cb319

Browse files
os-zhuangclaude
andauthored
fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220) (#3222)
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. Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE Co-authored-by: Claude <noreply@anthropic.com>
1 parent 50dfa4f commit 06cb319

4 files changed

Lines changed: 55 additions & 2 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/service-realtime": patch
3+
"@objectstack/metadata-core": patch
4+
---
5+
6+
fix(identity): close the generic-write apiMethods hole on sys_presence and sys_metadata (#3220)
7+
8+
Follow-through on #1591/#3213 (better-auth apiMethods reconciliation) for two
9+
non-better-auth managed objects that shipped the same contradiction: their
10+
`enable.apiMethods` advertised generic `create`/`update`/`delete` while their
11+
`managedBy` bucket forbids user-context writes, leaving the generic `/data`
12+
route open to a write the bucket does not permit.
13+
14+
- `sys_presence` (`managedBy: 'append-only'`) advertised `create`/`update`/`delete`
15+
(update/delete on an append-only object at that) but is written only over the
16+
realtime websocket/in-memory path, never through ObjectQL. Narrowed to
17+
`['get', 'list']`.
18+
- `sys_metadata` (`managedBy: 'system'`) advertised full CRUD but customization
19+
overlays are authored only through the metadata-protocol RPC (engine writes
20+
carry a transaction context, not a user session); neither the framework nor
21+
the Console (objectui) POSTs `/data/sys_metadata`. Narrowed to `['get', 'list']`.
22+
23+
Reads stay open. The metadata-protocol / realtime write paths are engine-level
24+
and bypass the HTTP exposure gate, so they are unaffected — verified by the
25+
metadata-authoring dogfood and the objectql overlay tests.
26+
27+
A blast-radius audit confirmed the broader `system`/`append-only` buckets are NOT
28+
safe to guard wholesale: several `system` objects (`sys_user_position`,
29+
`sys_user_permission_set`, `sys_position_permission_set`, `sys_user_preference`,
30+
`sys_import_job`) are legitimately user-writable by design (delegated
31+
administration, user preferences, imports). Generalizing the engine write guard
32+
to those buckets is intentionally NOT done here — see #3220 for the bucket-taxonomy
33+
root cause.

packages/metadata-core/src/objects/sys-metadata.object.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,13 @@ export const SysMetadataObject = ObjectSchema.create({
227227
trackHistory: true,
228228
searchable: false,
229229
apiEnabled: true,
230-
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
230+
// #3220 — sys_metadata is `managedBy: 'system'`: customization overlays are
231+
// authored ONLY through the metadata-protocol RPC (its engine writes carry a
232+
// transaction context, not a user session), never the generic /data route.
233+
// Neither the framework nor the Console (objectui) POSTs /data/sys_metadata,
234+
// so the generic write verbs were a latent hole for a user-context raw write
235+
// the bucket forbids. Reads stay open for the Setup "Data Model" grids.
236+
apiMethods: ['get', 'list'],
231237
trash: false,
232238
},
233239

packages/services/service-realtime/src/objects/sys-presence.object.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,12 @@ describe('SysPresence object definition', () => {
7070
it('should have API enabled', () => {
7171
expect(SysPresence.enable?.apiEnabled).toBe(true);
7272
});
73+
74+
it('exposes reads only — append-only, written over the realtime path, never /data (#3220)', () => {
75+
expect(SysPresence.enable?.apiMethods).toEqual(['get', 'list']);
76+
// Guard against re-introducing a generic write verb on an append-only object.
77+
for (const verb of ['create', 'update', 'delete', 'upsert']) {
78+
expect(SysPresence.enable?.apiMethods).not.toContain(verb);
79+
}
80+
});
7381
});

packages/services/service-realtime/src/objects/sys-presence.object.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ export const SysPresence = ObjectSchema.create({
113113
trackHistory: false,
114114
searchable: false,
115115
apiEnabled: true,
116-
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
116+
// #3220 — sys_presence is `managedBy: 'append-only'` and is written only
117+
// over the realtime (websocket/in-memory) path, never through ObjectQL.
118+
// Advertising create/update/delete here (and update/delete on an
119+
// append-only object at that) was a latent hole: it left the generic
120+
// /data route open for a user-context write the bucket forbids. Reads
121+
// stay open for diagnostic list views.
122+
apiMethods: ['get', 'list'],
117123
trash: false,
118124
mru: false,
119125
},

0 commit comments

Comments
 (0)