{"record":{"id":"530d87923ecfc276","repo":"hcengineering/platform","slug":"forbidden-530d87","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":"ApiError","httpStatus":403,"severity":"error","filePath":"services/export/pod-export/src/server.ts","lineNumber":311,"sourceCode":"      const format = parseExportFormat(req.query.format)\n\n      const {\n        _class,\n        query,\n        attributesOnly\n      }: {\n        _class: Ref<Class<Doc<Space>>>\n        query?: DocumentQuery<Doc>\n        attributesOnly: boolean\n      } = req.body\n\n      if (_class == null) {\n        throw new ApiError(400, 'Missing required parameters')\n      }\n\n      const decodedToken = decodeToken(token)\n      if (decodedToken.extra?.readonly !== undefined) {\n        throw new ApiError(403, 'Forbidden')\n      }\n      const isAdmin: boolean = decodedToken.extra?.admin === 'true'\n\n      const accountClient = getClient(envConfig.AccountsUrl, token)\n\n      try {\n        const info = await accountClient.getLoginWithWorkspaceInfo()\n        const winfo = info.workspaces[decodedToken.workspace]\n        if (!isAdmin) {\n          if (winfo === undefined) {\n            res.status(401).end('Invalid workspace')\n            return\n          } else {\n            if (winfo.role !== AccountRole.Owner) {\n              res.status(401).end('Not an owner of workspace')\n              return\n            }\n          }","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/export/pod-export/src/server.ts#L293-L329","documentation":"After decoding the request token, the export endpoint checks decodedToken.extra.readonly. If that flag is present (not undefined), the token is a read-only token and the (mutating/heavy) export operation is refused with 403 Forbidden. Presence of the flag — not its value — triggers the rejection.","triggerScenarios":"Calling the export endpoint with a token that was issued with extra.readonly set (e.g. a restricted/read-only workspace token); the check runs after the 'Missing required parameters' 400, so the body must already contain _class.","commonSituations":"CI pipelines configured with a deliberately restricted token; sharing a read-only demo token in documentation; a recent platform change issuing tokens with extra.readonly for external integrations.","solutions":["Request a non-read-only workspace token (one without extra.readonly) from the account/token service.","Inspect your token payload (decode it) to confirm extra.readonly is set, and regenerate the token without that restriction.","If read-only tokens should be allowed to export, change the server check — this is a policy decision, not a client fix.","Use an admin-capable token (extra.admin) if the operation requires elevated rights downstream."],"exampleFix":"// before\nconst token = issueToken(wsId, { extra: { readonly: 'true' } })\n// after\nconst token = issueToken(wsId, { extra: {} })","handlingStrategy":"try-catch","validationCode":"const decoded = decodeTokenSafe(token)\nif (decoded?.extra?.readonly !== undefined) {\n  throw new Error('Token is read-only; request a non-readonly token for exports')\n}","typeGuard":"function isWritableToken(t: { extra?: { readonly?: string } }): boolean {\n  return t.extra?.readonly === undefined\n}","tryCatchPattern":"try {\n  await exportWorkspace(params)\n} catch (e) {\n  if (e instanceof ApiError && e.status === 403 && e.message === 'Forbidden') {\n    token = await issueWritableWorkspaceToken(wsId)\n    return exportWorkspace({ ...params, token })\n  }\n  throw e\n}","preventionTips":["Keep separate tokens for read-only integrations and export jobs.","Decode and assert token capabilities (extra.admin/extra.readonly) at job startup.","Do not set extra.readonly on tokens minted for automation pipelines that must export."],"tags":["auth","http-403","forbidden","readonly-token"],"backgroundTag":"forbidden-readonly-token","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}