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
- 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.
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
- 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.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Unauthorized
- Method '${methodName}' not found in service implementation
- Current user is not allowed to create trainings
- account.status.WorkspaceNotFound
- AccountNotFound
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/789dad327c261225.
Report an issue: GitHub.