From a5e7af1a5347a2a6b8b2127c23b8b936ce8528b8 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Wed, 4 Mar 2026 16:27:52 -0800 Subject: [PATCH 1/4] Harden admin token creation to enforce target user role --- src/app/api/admin/api-tokens/route.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/api/admin/api-tokens/route.ts b/src/app/api/admin/api-tokens/route.ts index aba57ee..4dd11e7 100644 --- a/src/app/api/admin/api-tokens/route.ts +++ b/src/app/api/admin/api-tokens/route.ts @@ -18,7 +18,7 @@ const CreateTokenSchema = z.object({ name: z.string().min(1).max(100), expiresAt: z.string().datetime().nullable().optional(), userId: z.string().uuid().optional(), // Admin can specify which user the token acts as - role: z.enum(['admin', 'user']).optional(), // Admin can override the token role + role: z.enum(['admin', 'user']).optional(), // Accepted for compatibility, but cannot differ from target user role }); /** @@ -66,7 +66,8 @@ export async function GET(request: NextRequest) { /** * POST /api/admin/api-tokens - * Create a new API token. Admin can optionally specify userId and role. + * Create a new API token. Admin can optionally specify userId. + * Token role is always derived from the target user's current role. * Returns the full token ONCE. */ export async function POST(request: NextRequest) { @@ -120,19 +121,26 @@ export async function POST(request: NextRequest) { ); } - // Determine token role (defaults to target user's role) - const tokenRole = role || targetUser.role; - - // Log when admin explicitly overrides role to differ from user's actual role + // Security guard: token role must always match the target user's persisted role. + // This avoids role/identity mismatch (for example: acting as user A with admin role). if (role && role !== targetUser.role) { - logger.warn('Admin creating token with role different from user actual role', { - tokenRole: role, + logger.warn('Admin attempted token role override that differs from target user role', { + requestedRole: role, userActualRole: targetUser.role, targetUser: targetUser.plexUsername, createdBy: req.user!.username, }); + + return NextResponse.json( + { + error: `Token role must match target user's role (${targetUser.role}).`, + }, + { status: 400 } + ); } + const tokenRole = targetUser.role; + // Generate the token const { fullToken, tokenHash, tokenPrefix } = generateApiToken(); From 81813dc6255e41a7447abc721d88642016deca81 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Wed, 4 Mar 2026 16:37:00 -0800 Subject: [PATCH 2/4] Fix token UI success handling, fetch error surfacing, and docs key stability --- src/app/admin/settings/tabs/ApiTab/ApiTab.tsx | 16 +++++++------ src/app/api-docs/page.tsx | 2 +- src/components/profile/ApiTokensSection.tsx | 2 ++ src/lib/hooks/useApiTokens.ts | 24 +++++++++++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/app/admin/settings/tabs/ApiTab/ApiTab.tsx b/src/app/admin/settings/tabs/ApiTab/ApiTab.tsx index f57bfb0..30508d8 100644 --- a/src/app/admin/settings/tabs/ApiTab/ApiTab.tsx +++ b/src/app/admin/settings/tabs/ApiTab/ApiTab.tsx @@ -47,9 +47,9 @@ export function ApiTab() { const extraBody: Record = {}; if (newTokenUserId) extraBody.userId = newTokenUserId; if (newTokenRole) extraBody.role = newTokenRole; - await api.handleCreate(extraBody); - // Reset admin-specific fields on success - if (!api.error) { + const created = await api.handleCreate(extraBody); + // Reset admin-specific fields only when create succeeds + if (created) { setNewTokenUserId(''); setNewTokenRole(''); } @@ -123,10 +123,12 @@ export function ApiTab() { -