hcengineering/platform · error · PlatformError

Forbidden

Forbidden

Error message

Forbidden

What it means

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.

Source

Thrown at server/account/src/utils.ts:871

      role: AccountRole.Admin
    }
  }

  let role = await db.getWorkspaceRole(accountUuid, workspace.uuid)
  if (role == null && extra?.admin === 'true') {
    role = AccountRole.Admin
  }
  let account = await db.account.findOne({ uuid: accountUuid })

  if ((role == null || account == null) && workspace.allowReadOnlyGuest) {
    accountUuid = readOnlyGuestAccountUuid
    role = await db.getWorkspaceRole(accountUuid, workspace.uuid)
    account = await db.account.findOne({ uuid: accountUuid })
  }

  if (role == null) {
    ctx.error('Not a member of the workspace being selected', { workspaceUrl, accountUuid })
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

  if (accountUuid !== systemAccountUuid && account == null) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
  }

  if (accountUuid !== systemAccountUuid && meta !== undefined) {
    void setTimezone(ctx, db, accountUuid, account, meta)
  }

  if (role === AccountRole.ReadOnlyGuest) {
    if (extra == null) {
      extra = {}
    }
    extra.readonly = 'true'
  }

  const wsStatus = await db.workspaceStatus.findOne({ workspaceUuid: workspace.uuid })

View on GitHub (pinned to 63e28dc964)

Solutions

  1. 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.
  2. Make sure the client passes the workspaceUrl of a workspace it already has a token/membership for, not an arbitrary one.
  3. If guests should be able to join, enable allowReadOnlyGuest on the workspace so the fallback read-only guest account path is used.
  4. After role changes (removal/re-assignment), obtain a fresh token by logging in again instead of reusing cached credentials.

Example fix

// before: selecting a workspace without membership
await client.selectWorkspace(token, 'https://other-org.example.com')

// after: verify membership first, fall back to correct workspace
const ws = await client.listWorkspaces(token) // workspaces this account belongs to
if (!ws.some(w => w.url === targetUrl)) {
  throw new Error(`Not a member of ${targetUrl}; request an invite first.`)
}
await client.selectWorkspace(token, targetUrl)
Defensive patterns

Strategy: validation

Validate before calling

const ws = await accountClient.listWorkspaces(token) // workspaces the account belongs to
if (!ws.some(w => w.url === targetWorkspaceUrl)) {
  throw new Error(`Account is not a member of ${targetWorkspaceUrl}`)
}
await accountClient.selectWorkspace(token, targetWorkspaceUrl)

Type guard

function isMemberWorkspace(ws: { url: string } | null | undefined, url: string): ws is { url: string } {
  return ws != null && ws.url === url
}

Try / catch

try {
  await accountClient.selectWorkspace(token, url)
} catch (e) {
  if ((e as PlatformError).status.code === platform.status.Forbidden) {
    redirect('/no-access') // prompt to request an invite
  } else throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/2128749179e96155. Report an issue: GitHub.