{"record":{"id":"789dad327c261225","repo":"hcengineering/platform","slug":"account-status-forbidden-789dad","errorCode":"account.status.Forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"error_code","errorClass":"PlatformError","httpStatus":null,"severity":"error","filePath":"server/account/src/serviceOperations.ts","lineNumber":97,"sourceCode":"\n// Move to config?\nconst processingTimeoutMs = 30 * 1000\n\nexport async function listWorkspaces (\n  ctx: MeasureContext,\n  db: AccountDB,\n  branding: Branding | null,\n  token: string,\n  params: {\n    region?: string | null\n    mode?: WorkspaceMode | null\n  }\n): Promise<WorkspaceInfoWithStatus[]> {\n  const { region, mode } = params\n  const { extra } = decodeTokenVerbose(ctx, token)\n\n  if (!['tool', 'backup', 'admin', 'github'].includes(extra?.service) && extra?.admin !== 'true') {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n  }\n\n  return await getWorkspaces(db, false, region, mode)\n}\n\nexport async function listAccounts (\n  ctx: MeasureContext,\n  db: AccountDB,\n  branding: Branding | null,\n  token: string,\n  params: { search?: string, skip?: number, limit?: number }\n): Promise<AccountAggregatedInfo[]> {\n  const { extra } = decodeTokenVerbose(ctx, token)\n  const isAdmin = extra?.admin === 'true'\n\n  if (!isAdmin) {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n  }","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/serviceOperations.ts#L79-L115","documentation":"Forbidden is thrown by listWorkspaces when the token's extra claims neither contain a service in ['tool','backup','admin','github'] nor admin === 'true'. This operation is restricted to privileged service/tool tokens; ordinary user tokens are rejected.","triggerScenarios":"Calling listWorkspaces with a regular login token whose extra.service is missing or not one of tool/backup/admin/github and whose extra.admin is not the string 'true'.","commonSituations":"Frontend code mistakenly calls an admin-only endpoint; token minted without extra claims; admin flag passed as boolean true instead of string 'true'; service name typo (e.g. 'tools') not in the allow-list.","solutions":["Mint the token with extra.service set to one of 'tool','backup','admin','github', or with extra: { admin: 'true' } (exact string).","Use the dedicated admin/service account token instead of a user token for this call.","Fix the extra-claim casing/type — admin must be the string 'true', not boolean."],"exampleFix":"// before\nconst token = await generateToken(ctx, accountUuid, {}) // no extra claims\n// after\nconst token = await generateToken(ctx, accountUuid, { extra: { admin: 'true' } })","handlingStrategy":"validation","validationCode":"const { extra } = decodeTokenVerbose(ctx, token)\nconst allowed = ['tool', 'backup', 'admin', 'github'].includes(extra?.service) || extra?.admin === 'true'\nif (!allowed) {\n  throw new Error('listWorkspaces requires a tool/backup/admin/github service token or admin=true extra claim')\n}","typeGuard":"function isPrivilegedTokenExtra(extra: Record<string, string> | undefined): boolean {\n  return ['tool', 'backup', 'admin', 'github'].includes(extra?.service ?? '') || extra?.admin === 'true'\n}","tryCatchPattern":"try {\n  const workspaces = await accountClient.listWorkspaces(token, { region, mode })\n} catch (err) {\n  if (isPlatformError(err) && err.status.code === account.status.Forbidden) {\n    // route to an admin/service token issuance flow\n  }\n  throw err\n}","preventionTips":["Mint admin tokens with extra: { admin: 'true' } (string, not boolean).","Keep service names exactly as 'tool','backup','admin','github'.","Never expose listWorkspaces to end-user tokens; keep it server-side behind service auth."],"tags":["authorization","admin","token-claims","service"],"backgroundTag":"missing-privileged-token-claims","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}