{"record":{"id":"f95ee2e1dfd718df","repo":"toeverything/AFFiNE","slug":"space-access-denied-f95ee2","errorCode":"space_access_denied","errorMessage":"You do not have permission to access Space ${spaceId}.","messagePattern":"You do not have permission to access Space (.+?)\\.","errorType":"exception","errorClass":"SpaceAccessDenied","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/indexer/service.ts","lineNumber":160,"sourceCode":"        )\n      );\n      for (const doc of docs) {\n        if (!doc.title) doc.title = titles.get(doc.docId) ?? '';\n      }\n    }\n    const users = await this.models.user.getPublicUsersMap(userIds);\n    for (const doc of docs) {\n      doc.createdByUser = users.get(doc.createdByUserId);\n      doc.updatedByUser = users.get(doc.updatedByUserId);\n    }\n    return docs;\n  }\n\n  private unwrap<T>(output: SearchOperationOutput, workspaceId: string): T {\n    if (output.ok) return output.value as T;\n    switch (output.errorCode) {\n      case 'workspace_denied':\n        throw new SpaceAccessDenied({ spaceId: workspaceId });\n      case 'invalid_request':\n      case 'unsupported_query':\n        throw new InvalidIndexerInput({ reason: output.errorCode });\n      case 'provider_unavailable':\n        throw new SearchProviderUnavailable();\n      case 'index_not_ready':\n        throw new SearchIndexNotReady({ spaceId: workspaceId });\n      case 'permission_syncing':\n        throw new SearchPermissionSyncing();\n      case 'index_failed':\n        throw new SearchIndexFailed({\n          diagnosticId: 'search_workspace_reconcile_failed',\n        });\n      default:\n        throw new InternalServerError();\n    }\n  }\n}","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b6de0ad51b76f3daac2d3d6325369ea623ed7ed4/packages/backend/server/src/plugins/indexer/service.ts#L142-L178","documentation":"IndexerService.unwrap translates search-operation results for the caller. An output with errorCode 'workspace_denied' is mapped to SpaceAccessDenied (code space_access_denied) with the queried workspaceId: the permission check concluded the current user has no access to that workspace.","triggerScenarios":"Calling an indexer search for a workspaceId the requesting user is not a member of, or with a session/token that belongs to a different user; also stale permissions after the user's membership was revoked mid-session.","commonSituations":"Expired or mismatched auth context attached to the search call; permissions changed while a client kept a workspace open; passing another workspace's id in the query (e.g. stale route param); invoking admin/indexer endpoints with a regular user token.","solutions":["Verify the caller's membership/permission for workspaceId before invoking search","Refresh the session/token and retry to rule out a stale auth context","Check that workspaceId in the query is actually the workspace the client has open"],"exampleFix":"// before\nconst docs = await indexerService.search(user, { workspaceId, query });\n// after: gate on permission first\nconst allowed = await permissionService.tryCheck(workspaceId, user.id);\nif (!allowed) throw new SpaceAccessDenied({ spaceId: workspaceId });\nconst docs = await indexerService.search(user, { workspaceId, query });","handlingStrategy":"validation","validationCode":"// check membership before calling indexer search\nconst permission = await permissionService.get(workspaceId, user.id);\nif (!permission || !permission.canRead) {\n  return forbidden(spaceId);\n}\nconst docs = await indexerService.search(user, { workspaceId, query });","typeGuard":null,"tryCatchPattern":"// distinguish denial from provider/permission availability\ntry {\n  return await indexerService.search(user, { workspaceId, query });\n} catch (e) {\n  if (e instanceof SpaceAccessDenied) { // code === 'space_access_denied'\n    return fortyThree({ spaceId: e.spaceId }); // 403, do not retry\n  }\n  throw e; // provider/permission-unavailable handled separately\n}","preventionTips":["Run the permission check in the same request that opens a workspace, and reuse its result for search","Re-check permissions on focus/resume — membership may have been revoked while the client was open","Never forward a workspaceId taken from client input without scoping it to the session's accessible workspaces"],"tags":["indexer","search","permission","workspace","access-denied"],"backgroundTag":"permission-denied","analyzedSha":"b6de0ad51b76f3daac2d3d6325369ea623ed7ed4","analyzedAt":"2026-08-21T18:20:45.039Z","contentChangedAt":"2026-08-21T18:20:45.039Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}