{"record":{"id":"d6dfd9bbec344713","repo":"hcengineering/platform","slug":"forbidden-d6dfd9","errorCode":"Forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"exception","errorClass":"PlatformError","httpStatus":null,"severity":"error","filePath":"server/account/src/serviceOperations.ts","lineNumber":273,"sourceCode":" * on every progress update.\n * If no progress is reported for the workspace during this time,\n * it will become available again to be processed by another executor.\n */\nexport async function getPendingWorkspace (\n  ctx: MeasureContext,\n  db: AccountDB,\n  branding: Branding | null,\n  token: string,\n  params: {\n    region: string\n    version: Data<Version>\n    operation: WorkspaceOperation\n  }\n): Promise<WorkspaceInfoWithStatus | undefined> {\n  const { region, version, operation } = params\n  const { extra } = decodeTokenVerbose(ctx, token)\n  if (extra?.service !== 'workspace') {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n  }\n\n  const wsLivenessDays = getMetadata(accountPlugin.metadata.WsLivenessDays)\n  const wsLivenessMs = wsLivenessDays !== undefined ? wsLivenessDays * 24 * 60 * 60 * 1000 : undefined\n\n  const result = await db.getPendingWorkspace(region, version, operation, processingTimeoutMs, wsLivenessMs)\n\n  if (result != null) {\n    ctx.info('getPendingWorkspace', {\n      workspaceId: result.uuid,\n      workspaceName: result.name,\n      dataId: result.dataId,\n      mode: result.status.mode,\n      operation,\n      region,\n      major: result.status.versionMajor,\n      minor: result.status.versionMinor,\n      patch: result.status.versionPatch,","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/serviceOperations.ts#L255-L291","documentation":"Forbidden is thrown by getPendingWorkspace when the decoded token's extra service claim is not exactly 'workspace'. This internal operation is reserved for the workspace service; any other service (e.g. 'tool', 'account', user tokens) is rejected before the pending-workspace query runs. It is a service-to-service authorization check, not a user permission failure.","triggerScenarios":"Invoking getPendingWorkspace with a token whose extra.service != 'workspace' — e.g. using a tool-service token, an unauthenticated/user token with no service claim, or a token issued for the wrong service.","commonSituations":"Misconfigured service-to-service auth where the workspace worker reuses a generic service token; a new microservice calling workspace maintenance APIs with its own service claim; upgrading/rotating tokens and losing the 'workspace' service identifier; calling the endpoint from client-side code with a user token.","solutions":["Obtain/sign the token with extra.service === 'workspace' (use the workspace service's own credentials/keys).","Check token issuance config so the service claim is set correctly for internal calls (not defaulted to another service).","If you are another service, call the operation through the workspace service instead of directly.","Decode the token locally (decodeTokenVerbose) and assert extra.service before making the call to fail fast."],"exampleFix":"// before\nconst token = generateToken(ctx, 'account', { service: 'tool' })\nawait accountClient.getPendingWorkspace(ctx, token, params)\n\n// after\nconst token = generateServiceToken(ctx, 'account', { service: 'workspace' }) // must be the workspace service\nawait accountClient.getPendingWorkspace(ctx, token, params)","handlingStrategy":"try-catch","validationCode":"// Pre-check the service claim before calling\nconst { extra } = decodeTokenVerbose(ctx, token)\nif (extra?.service !== 'workspace') {\n  throw new Error('getPendingWorkspace requires a workspace-service token')\n}","typeGuard":"function isWorkspaceServiceToken(extra: TokenExtra | undefined): extra is TokenExtra & { service: 'workspace' } {\n  return extra?.service === 'workspace'\n}","tryCatchPattern":"try {\n  return await getPendingWorkspace(params)\n} catch (err) {\n  if (err instanceof PlatformError && err.status.code === platform.status.Forbidden) {\n    throw new Error('Token service must be \"workspace\"; re-mint token with correct service claim')\n  }\n  throw err\n}","preventionTips":["Use the workspace service's dedicated credentials when signing internal tokens.","Assert extra.service after decoding tokens in a shared auth helper.","Never reuse user or tool tokens for workspace-internal endpoints.","Add integration tests that assert token service claims for internal APIs."],"tags":["authorization","forbidden","service-token","internal-api"],"backgroundTag":"insufficient-service-permissions","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}