{"record":{"id":"2128749179e96155","repo":"hcengineering/platform","slug":"forbidden-212874","errorCode":"Forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"error_code","errorClass":"PlatformError","httpStatus":403,"severity":"error","filePath":"server/account/src/utils.ts","lineNumber":871,"sourceCode":"      role: AccountRole.Admin\n    }\n  }\n\n  let role = await db.getWorkspaceRole(accountUuid, workspace.uuid)\n  if (role == null && extra?.admin === 'true') {\n    role = AccountRole.Admin\n  }\n  let account = await db.account.findOne({ uuid: accountUuid })\n\n  if ((role == null || account == null) && workspace.allowReadOnlyGuest) {\n    accountUuid = readOnlyGuestAccountUuid\n    role = await db.getWorkspaceRole(accountUuid, workspace.uuid)\n    account = await db.account.findOne({ uuid: accountUuid })\n  }\n\n  if (role == null) {\n    ctx.error('Not a member of the workspace being selected', { workspaceUrl, accountUuid })\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n  }\n\n  if (accountUuid !== systemAccountUuid && account == null) {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))\n  }\n\n  if (accountUuid !== systemAccountUuid && meta !== undefined) {\n    void setTimezone(ctx, db, accountUuid, account, meta)\n  }\n\n  if (role === AccountRole.ReadOnlyGuest) {\n    if (extra == null) {\n      extra = {}\n    }\n    extra.readonly = 'true'\n  }\n\n  const wsStatus = await db.workspaceStatus.findOne({ workspaceUuid: workspace.uuid })","sourceCodeStart":853,"sourceCodeEnd":889,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/utils.ts#L853-L889","documentation":"During workspace selection (selectWorkspace in server/account/src/utils.ts), the account service verifies the caller actually holds a role in the target workspace. When db.getWorkspaceRole returns null for both the requesting account and (if the workspace allows it) the read-only guest account, it means the account is not a member of the workspace it tried to select, so a Forbidden PlatformError is thrown and no workspace token is issued.","triggerScenarios":"Calling the selectWorkspace endpoint with a workspaceUrl whose workspace exists but where db.getWorkspaceRole(accountUuid, workspace.uuid) returns null and the workspace does not allow read-only guests (or the guest account also has no role). Happens when a token for one workspace is reused to select another workspace the user was never invited to, or after the user's membership was removed.","commonSituations":"A client caches an old workspace URL/token after being removed from the workspace; a user types or bookmarks another org's workspace URL; a deployment/migration moved data so role assignments (workspace membership) are missing; integrations hardcoding a workspaceUrl the service account has no role in.","solutions":["Confirm the account is actually a member: check the workspace role via the account API or db.getWorkspaceRole and re-invite/assign the user to the workspace if missing.","Make sure the client passes the workspaceUrl of a workspace it already has a token/membership for, not an arbitrary one.","If guests should be able to join, enable allowReadOnlyGuest on the workspace so the fallback read-only guest account path is used.","After role changes (removal/re-assignment), obtain a fresh token by logging in again instead of reusing cached credentials."],"exampleFix":"// before: selecting a workspace without membership\nawait client.selectWorkspace(token, 'https://other-org.example.com')\n\n// after: verify membership first, fall back to correct workspace\nconst ws = await client.listWorkspaces(token) // workspaces this account belongs to\nif (!ws.some(w => w.url === targetUrl)) {\n  throw new Error(`Not a member of ${targetUrl}; request an invite first.`)\n}\nawait client.selectWorkspace(token, targetUrl)","handlingStrategy":"validation","validationCode":"const ws = await accountClient.listWorkspaces(token) // workspaces the account belongs to\nif (!ws.some(w => w.url === targetWorkspaceUrl)) {\n  throw new Error(`Account is not a member of ${targetWorkspaceUrl}`)\n}\nawait accountClient.selectWorkspace(token, targetWorkspaceUrl)","typeGuard":"function isMemberWorkspace(ws: { url: string } | null | undefined, url: string): ws is { url: string } {\n  return ws != null && ws.url === url\n}","tryCatchPattern":"try {\n  await accountClient.selectWorkspace(token, url)\n} catch (e) {\n  if ((e as PlatformError).status.code === platform.status.Forbidden) {\n    redirect('/no-access') // prompt to request an invite\n  } else throw e\n}","preventionTips":["Only select workspaces returned by the account service for the current account","Refresh membership state after invitations/removals instead of caching","Handle Forbidden by showing a 'request access' UI rather than retrying","For guest flows, enable allowReadOnlyGuest on the workspace beforehand"],"tags":["authorization","workspace-membership","account-service"],"backgroundTag":"workspace-forbidden","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}