Skip to content

Commit bccf7c0

Browse files
committed
fix(security): /me/permissions folds wildcard super-user into per-object FLS + bump objectui to sys_user edit-form fix (ADR-0092 D4)
Completes ADR-0092 D4 end-to-end (#2817). Three parts brought the standard sys_user edit form to life for platform admins, verified in a running Console (edit name → persisted; email/role stay read-only): 1. foldWildcardSuperUser in /me/permissions (hono-plugin): the per-object FLS map now folds the '*' modifyAll/viewAll super-user grant into every object entry, so the client mirrors the server's actual enforcement (checkObjectPermission is most-permissive, no deny-wins). Fixes the false-negative where a platform admin who also holds organization_admin (denies identity writes) saw sys_user.allowEdit:false and a disabled form, though the server accepts the write (PATCH → 200). ADR-0057 D10. 2. .objectui-sha bump to 5da9905 — pulls in objectui#2395 (ObjectForm honors userActions.edit instead of blanket-disabling managed objects). 3. sys_user userActions.edit + field readonly governance (earlier commit). The identity write guard (#2828) still restricts the actual write to {name, image}; these changes only correct which affordances the UI shows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGAN5VvvBi4YRGwpNT6e21
1 parent 6696295 commit bccf7c0

5 files changed

Lines changed: 129 additions & 1 deletion

File tree

.changeset/console-5da9905b30fc.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/console": patch
3+
---
4+
5+
Console (objectui) refreshed to `5da9905b30fc`. Frontend changes in this range:
6+
7+
- fix(plugin-form): honor userActions.edit on managed objects, don't blanket-disable fields (ADR-0092 D4) (#2395)
8+
9+
objectui range: `6fa8e6aeb67c...5da9905b30fc`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/plugin-hono-server": patch
3+
---
4+
5+
fix(security): `/me/permissions` folds the `'*'` wildcard super-user grant into per-object FLS, matching server enforcement (ADR-0057 D10)
6+
7+
The `/api/v1/auth/me/permissions` per-object map merged each permission set's
8+
explicit `objects` entries most-permissively per key, but treated `'*'` and
9+
named objects as independent keys — so a wildcard "Modify/View All Data" grant
10+
was never propagated into a per-object entry another set explicitly denied.
11+
That made the client's field-level security STRICTER than the server's actual
12+
enforcement (`PermissionEvaluator.checkObjectPermission` allows as soon as any
13+
set grants, including via the `'*'` modifyAll/viewAll super-user bypass, with
14+
no deny-wins).
15+
16+
Concretely: a platform admin (`admin_full_access``'*': {modifyAllRecords}`)
17+
who also holds `organization_admin` (which denies writes on identity tables)
18+
resolved to `sys_user.allowEdit:false` in this payload, so the Console disabled
19+
the standard edit form — even though the server accepts the write (`PATCH
20+
/data/sys_user {name}` → 200). The new `foldWildcardSuperUser` post-pass lifts
21+
each per-object entry's read/write bits when the merged wildcard is a
22+
super-user grant, so the client mirrors the server (never broader — the
23+
super-user grant already covers private/managed objects server-side). This
24+
unblocks the ADR-0092 D4 `sys_user` profile-edit affordance for platform
25+
admins; the identity write guard still restricts the actual write to
26+
`{name, image}`.

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6fa8e6aeb67c53169b7c9c4bbc1ef7cf897d0cbf
1+
5da9905b30fc06fa2d262fbcbccbcf881f9da360
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { foldWildcardSuperUser } from './hono-plugin.js';
5+
6+
/**
7+
* ADR-0057 D10 / ADR-0092 D5 — the `/me/permissions` per-object FLS map must
8+
* mirror the server's actual enforcement, which grants writes via a `'*'`
9+
* modifyAll super-user bypass regardless of another set's explicit per-object
10+
* deny (most-permissive merge, no deny-wins).
11+
*/
12+
describe('foldWildcardSuperUser', () => {
13+
it('lifts an explicit per-object deny when the wildcard is a modifyAll super-user grant', () => {
14+
const objects: Record<string, any> = {
15+
'*': { allowRead: true, allowEdit: true, viewAllRecords: true, modifyAllRecords: true },
16+
// As produced when admin_full_access ('*') composes with organization_admin
17+
// (explicit sys_user deny) — the naive merge leaves allowEdit:false.
18+
sys_user: { allowRead: true, allowEdit: false, allowCreate: false, allowDelete: false },
19+
};
20+
foldWildcardSuperUser(objects);
21+
expect(objects.sys_user).toMatchObject({ allowRead: true, allowEdit: true, allowCreate: true, allowDelete: true });
22+
// The wildcard entry itself is untouched.
23+
expect(objects['*'].modifyAllRecords).toBe(true);
24+
});
25+
26+
it('viewAll-only wildcard lifts read but NOT write', () => {
27+
const objects: Record<string, any> = {
28+
'*': { allowRead: true, viewAllRecords: true },
29+
sys_session: { allowRead: false, allowEdit: false },
30+
};
31+
foldWildcardSuperUser(objects);
32+
expect(objects.sys_session.allowRead).toBe(true);
33+
expect(objects.sys_session.allowEdit).toBe(false);
34+
});
35+
36+
it('no-ops when the wildcard is not a super-user grant', () => {
37+
const objects: Record<string, any> = {
38+
'*': { allowRead: true, allowEdit: true }, // plain allow, no view/modifyAll
39+
sys_user: { allowRead: true, allowEdit: false },
40+
};
41+
foldWildcardSuperUser(objects);
42+
expect(objects.sys_user.allowEdit).toBe(false); // untouched — no super-user bypass
43+
});
44+
45+
it('no-ops when there is no wildcard entry', () => {
46+
const objects: Record<string, any> = { sys_user: { allowEdit: false } };
47+
foldWildcardSuperUser(objects);
48+
expect(objects.sys_user.allowEdit).toBe(false);
49+
});
50+
});

packages/plugins/plugin-hono-server/src/hono-plugin.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,45 @@ export interface HonoPluginOptions {
8181
* - `@objectstack/rest` → CRUD, metadata, discovery, UI, batch
8282
* - `createDispatcherPlugin()` → auth, graphql, analytics, packages, etc.
8383
*/
84+
85+
/**
86+
* Fold the `'*'` wildcard super-user grant into every per-object entry of a
87+
* `/me/permissions` `objects` map, mutating it in place.
88+
*
89+
* The endpoint merges each resolved permission set's explicit `objects` entries
90+
* most-permissively per key, but treats `'*'` and named objects as independent
91+
* keys — so a wildcard "Modify/View All Data" grant is never propagated into a
92+
* per-object entry another set explicitly denied. That makes the client's
93+
* per-object FLS STRICTER than the server's actual enforcement
94+
* (`PermissionEvaluator.checkObjectPermission`, which returns allow as soon as
95+
* ANY set grants — including via the `'*'` modifyAll/viewAll super-user bypass,
96+
* with no deny-wins). The mismatch surfaces for a platform admin
97+
* (`admin_full_access` `'*': {modifyAllRecords}`) who ALSO holds
98+
* `organization_admin` (which denies writes on identity tables): the client
99+
* would see `sys_user.allowEdit:false` and disable a form the server accepts
100+
* (verified: `PATCH /data/sys_user {name}` → 200). ADR-0057 D10 makes the
101+
* server the authoritative gate; the client must mirror it, never diverge.
102+
*
103+
* The super-user grant covers private/managed objects on the server, so folding
104+
* it here is exactly as broad as real enforcement — never broader.
105+
*/
106+
export function foldWildcardSuperUser(objects: Record<string, any>): void {
107+
const wild = objects?.['*'];
108+
if (!wild) return;
109+
const superRead = wild.viewAllRecords === true || wild.modifyAllRecords === true;
110+
const superWrite = wild.modifyAllRecords === true;
111+
if (!superRead && !superWrite) return;
112+
for (const [obj, acc] of Object.entries(objects) as Array<[string, any]>) {
113+
if (obj === '*' || !acc) continue;
114+
if (superRead) acc.allowRead = true;
115+
if (superWrite) {
116+
acc.allowEdit = true;
117+
acc.allowCreate = true;
118+
acc.allowDelete = true;
119+
}
120+
}
121+
}
122+
84123
export class HonoServerPlugin implements Plugin {
85124
name = 'com.objectstack.server.hono';
86125
type = 'server';
@@ -796,6 +835,10 @@ export class HonoServerPlugin implements Plugin {
796835
}
797836
}
798837
}
838+
// Fold the `'*'` wildcard super-user grant into every per-object
839+
// entry (see foldWildcardSuperUser) so the client's per-object FLS
840+
// matches the server's actual enforcement (ADR-0057 D10).
841+
foldWildcardSuperUser(objects);
799842
return c.json({
800843
authenticated: true,
801844
userId: execCtx.userId,

0 commit comments

Comments
 (0)