{"record":{"id":"b6f31ef65886d7f4","repo":"hcengineering/platform","slug":"forbidden-read-only-token","errorCode":null,"errorMessage":"Forbidden: read-only token","messagePattern":"Forbidden: read-only token","errorType":"http","errorClass":"ApiError","httpStatus":403,"severity":"error","filePath":"services/export/pod-export/src/server.ts","lineNumber":509,"sourceCode":"          measureCtx.warn(`Invalid targetWorkspace parameter: ${String(targetWorkspace)}`)\n          throw new ApiError(400, 'Missing or invalid required parameter: targetWorkspace')\n        }\n        if (_class == null || typeof _class !== 'string') {\n          measureCtx.warn(`Invalid _class parameter: ${String(_class)}`)\n          throw new ApiError(400, 'Missing or invalid required parameter: _class')\n        }\n        if (conflictStrategy !== undefined && conflictStrategy !== 'skip' && conflictStrategy !== 'duplicate') {\n          measureCtx.warn(`Invalid conflictStrategy: ${String(conflictStrategy)}`)\n          throw new ApiError(400, 'Invalid conflictStrategy. Must be \"skip\" or \"duplicate\"')\n        }\n        if (includeAttachments !== undefined && typeof includeAttachments !== 'boolean') {\n          measureCtx.warn(`Invalid includeAttachments: ${String(includeAttachments)}`)\n          throw new ApiError(400, 'Invalid includeAttachments. Must be boolean')\n        }\n\n        decodedToken = decodeToken(token)\n        if (decodedToken.extra?.readonly !== undefined) {\n          throw new ApiError(403, 'Forbidden: read-only token')\n        }\n\n        // Get target workspace info\n        const accountClient = getClient(envConfig.AccountsUrl, token)\n        const targetWsLoginInfo = await accountClient.getLoginWithWorkspaceInfo()\n\n        const targetWsInfo = targetWsLoginInfo.workspaces[targetWorkspace]\n        if (targetWsInfo === undefined) {\n          measureCtx.warn(`Target workspace not found or not accessible: ${targetWorkspace}`)\n          throw new ApiError(404, 'Target workspace not found or not accessible')\n        }\n\n        // Verify user has write access to target workspace\n        const isAdmin: boolean = decodedToken.extra?.admin === 'true'\n        if (!isAdmin && targetWsInfo.role !== AccountRole.Owner) {\n          measureCtx.warn(\n            `User does not have write access to target workspace: ${targetWorkspace}, role: ${targetWsInfo.role}`\n          )","sourceCodeStart":491,"sourceCodeEnd":527,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/export/pod-export/src/server.ts#L491-L527","documentation":"After decoding the auth token, the pod-export server rejects requests whose token carries an extra.readonly flag (any value). This HTTP 403 error exists because export creates data in the target workspace, which a read-only token is not permitted to do.","triggerScenarios":"Authenticating the export call with a token whose decoded `extra.readonly` property is defined (e.g. a read-only service account or restricted key), regardless of the actual flag value ('true' or even 'false').","commonSituations":"Ops scripts using a deliberately restricted read-only key to run an import/migration into another workspace; reusing a shared read-only token from monitoring/reporting tooling; a stale token minted with readonly scope.","solutions":["Obtain a non-read-only token (mint a new one without the readonly extra flag) and retry.","Use a full-permission account/token for cross-workspace export operations.","If the readonly flag is wrong, regenerate the token with correct scope configuration."],"exampleFix":"// before\ncurl -H 'Authorization: Bearer <readonly-token>' .../export\n// after\ncurl -H 'Authorization: Bearer <token-with-write-scope>' .../export","handlingStrategy":"try-catch","validationCode":"const decoded = decodeToken(token)\nif (decoded?.extra?.readonly !== undefined) {\n  throw new Error('Refusing to call export with a read-only token')\n}","typeGuard":"function isWritableToken(decoded: DecodedToken): boolean {\n  return decoded.extra?.readonly === undefined\n}","tryCatchPattern":"try {\n  await exportPod({ token })\n} catch (err) {\n  if (err instanceof ApiError && err.status === 403 && /read-only/i.test(err.message)) {\n    console.error('Swap to a token without the readonly flag and retry')\n  } else throw err\n}","preventionTips":["Keep read-only and write credentials in separate config keys and use the right one per operation.","Inspect decoded.extra.readonly at startup of migration scripts before doing work.","Don't reuse monitoring/reporting tokens for migration tooling."],"tags":["auth","permissions","forbidden"],"backgroundTag":"read-only-token-rejected","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}