diff --git a/.changeset/oauth-toggle-admin-gate.md b/.changeset/oauth-toggle-admin-gate.md new file mode 100644 index 0000000000..0eea68e991 --- /dev/null +++ b/.changeset/oauth-toggle-admin-gate.md @@ -0,0 +1,16 @@ +--- +'@objectstack/plugin-auth': patch +--- + +fix(auth): restore the admin gate on POST /admin/oauth-application/toggle-disabled after ADR-0068 + +ADR-0068 stopped `customSession` from synthesizing `user.role = 'admin'`; +canonical roles now arrive in `user.roles[]` with `user.isPlatformAdmin` as a +derived alias. The OAuth-client enable/disable route was missed in that +migration and still gated on `session.user.role !== 'admin'`, which now rejects +even platform admins (the scalar is no longer synthesized). It now mirrors the +sibling /admin/unlock-user gate: `isPlatformAdmin` / `platform_admin` in +`roles[]`, with the legacy `role` scalar as a fallback. + +Also corrects the now-stale `customSession()` doc comment in auth-manager that +still described the removed `user.role = 'admin'` overwrite. diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 4c332edd9d..b34d76ddf7 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -1539,18 +1539,19 @@ export class AuthManager { })); } - // customSession() — augments the session payload with a derived `role` - // field so frontend gating (e.g. AppShell's `isAdmin = user.role === 'admin'`) - // works without each consumer having to re-query permission sets. + // customSession() — augments the session payload with the canonical + // `roles: string[]` array (ADR-0068 D1/D2): the stored `user.role` scalar + // split on commas, PLUS the active membership mapped to canonical + // `org_owner`/`org_admin`/`org_member`, PLUS `platform_admin` when the user + // holds the admin_full_access permission set. `user.isPlatformAdmin` is a + // derived alias of `'platform_admin' in roles`. // - // It also returns a `roles: string[]` array: the stored `user.role` - // string split on commas (the admin plugin stores multi-role users as - // e.g. `"admin,manager"`), with `'admin'` appended (deduplicated) when - // the user is promoted below. Consumers that match on individual role - // names (e.g. the Console approvals inbox resolving `role:` - // approvers) must read `roles` — `user.role` is *replaced* by the - // literal `'admin'` on promotion, so business roles such as `manager` - // only survive in the array. + // IMPORTANT: `user.role` is NOT overwritten anymore — consumers must gate + // on `roles[]` / `isPlatformAdmin` (e.g. via objectui's useIsWorkspaceAdmin), + // never on `user.role === 'admin'`. Consumers that match individual role + // names (e.g. the Console approvals inbox resolving `role:` approvers) + // also read `roles` — business roles such as `manager` survive only there. + // The raw membership role stays on the organization plugin's `member` payload. // // Better-auth's `sys_user` table doesn't carry a `role` column. We derive // it from two sources: diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index f92b365ec8..8d2b09afc4 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -940,10 +940,18 @@ export class AuthPlugin implements Plugin { if (!session?.user?.id) { return c.json({ success: false, error: { code: 'unauthorized', message: 'Sign in first' } }, 401); } - // The customSession plugin synthesizes `user.role = 'admin'` for - // platform admins (admin_full_access permission set) and active-org - // owners/admins; anyone else is denied. - if ((session.user as any).role !== 'admin') { + // Platform-admin gate. ADR-0068 removed the `user.role = 'admin'` + // synthesis, so a stale `role === 'admin'` check now rejects even + // platform admins. Accept the canonical signals customSession carries + // (the derived `isPlatformAdmin` alias / `platform_admin` in roles[]), + // with the legacy admin-plugin `role` scalar as a fallback. Mirrors the + // /admin/unlock-user gate below. + const u: any = session.user; + const isAdmin = + u?.isPlatformAdmin === true || + (Array.isArray(u?.roles) && u.roles.includes('platform_admin')) || + u?.role === 'admin'; + if (!isAdmin) { return c.json({ success: false, error: { code: 'forbidden', message: 'Admin role required' } }, 403); }