Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/oauth-toggle-admin-gate.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 12 additions & 11 deletions packages/plugins/plugin-auth/src/auth-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<name>`
// 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:<name>` 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:
Expand Down
16 changes: 12 additions & 4 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down