hcengineering/platform · error · PlatformError

account.status.Forbidden

account.status.Forbidden

Error message

Forbidden

What it means

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.

Source

Thrown at server/account/src/serviceOperations.ts:97

// Move to config?
const processingTimeoutMs = 30 * 1000

export async function listWorkspaces (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: {
    region?: string | null
    mode?: WorkspaceMode | null
  }
): Promise<WorkspaceInfoWithStatus[]> {
  const { region, mode } = params
  const { extra } = decodeTokenVerbose(ctx, token)

  if (!['tool', 'backup', 'admin', 'github'].includes(extra?.service) && extra?.admin !== 'true') {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

  return await getWorkspaces(db, false, region, mode)
}

export async function listAccounts (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: { search?: string, skip?: number, limit?: number }
): Promise<AccountAggregatedInfo[]> {
  const { extra } = decodeTokenVerbose(ctx, token)
  const isAdmin = extra?.admin === 'true'

  if (!isAdmin) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Mint the token with extra.service set to one of 'tool','backup','admin','github', or with extra: { admin: 'true' } (exact string).
  2. Use the dedicated admin/service account token instead of a user token for this call.
  3. Fix the extra-claim casing/type — admin must be the string 'true', not boolean.

Example fix

// before
const token = await generateToken(ctx, accountUuid, {}) // no extra claims
// after
const token = await generateToken(ctx, accountUuid, { extra: { admin: 'true' } })
Defensive patterns

Strategy: validation

Validate before calling

const { extra } = decodeTokenVerbose(ctx, token)
const allowed = ['tool', 'backup', 'admin', 'github'].includes(extra?.service) || extra?.admin === 'true'
if (!allowed) {
  throw new Error('listWorkspaces requires a tool/backup/admin/github service token or admin=true extra claim')
}

Type guard

function isPrivilegedTokenExtra(extra: Record<string, string> | undefined): boolean {
  return ['tool', 'backup', 'admin', 'github'].includes(extra?.service ?? '') || extra?.admin === 'true'
}

Try / catch

try {
  const workspaces = await accountClient.listWorkspaces(token, { region, mode })
} catch (err) {
  if (isPlatformError(err) && err.status.code === account.status.Forbidden) {
    // route to an admin/service token issuance flow
  }
  throw err
}

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/789dad327c261225. Report an issue: GitHub.