{"record":{"id":"9e63b5f9bfeb0584","repo":"toeverything/AFFiNE","slug":"space-access-denied","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/core/permission/service.ts","lineNumber":110,"sourceCode":"    workspaceId: string;\n    action: PermissionWorkspaceAction;\n    allowLocal?: boolean;\n  }) {\n    const output = await this.workspacePermissions({\n      ...input,\n      actions: [input.action],\n    });\n    return output.decisions[0]?.allowed ?? false;\n  }\n\n  async assertWorkspace(input: {\n    userId?: string;\n    workspaceId: string;\n    action: PermissionWorkspaceAction;\n    allowLocal?: boolean;\n  }) {\n    if (!(await this.canWorkspace(input))) {\n      throw new SpaceAccessDenied({ spaceId: input.workspaceId });\n    }\n  }\n\n  async docPermissions(input: {\n    userId?: string;\n    workspaceId: string;\n    docId: string;\n    actions: PermissionDocAction[];\n    allowLocal?: boolean;\n  }) {\n    const output = await this.evaluateLoaded({\n      userId: input.userId,\n      workspaceId: input.workspaceId,\n      docs: [{ docId: input.docId, actions: input.actions }],\n      allowLocal: input.allowLocal,\n    });\n    const doc = output.docs[0];\n    return {","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/591f874dad30887a80143a061a44bd3ca7ee3299/packages/backend/server/src/core/permission/service.ts#L92-L128","documentation":"PermissionService.assertWorkspace evaluates the user's OpenAccess decisions for a workspace-level action (via canWorkspace) and throws SpaceAccessDenied (no_permission / space_access_denied, message 'You do not have permission to access Space <id>.') when the decision is not allowed. It is the standard guard behind workspace controllers, the sync gateway, and quota realtime handlers.","triggerScenarios":"Calling a workspace API with a userId that is not a member (or an anonymous/undefined userId when auth is missing); a member performing an owner/admin-only action (e.g. deleting the workspace); membership revoked or invitation expired while the client keeps a valid-looking session; sync-gateway requests missing the required session context (allowLocal not honored).","commonSituations":"Tokens from one environment used against another; permission cache staleness after role changes; scripts hitting internal endpoints without the internal-auth header; frontends forgetting to pass auth context on new endpoints.","solutions":["Verify the user's membership/role for the workspace before making the call (canWorkspace instead of assertWorkspace)","Re-authenticate and refresh the session if membership changed recently","Request the needed role from the workspace owner, or use an account with sufficient permission","For internal/service callers, send the proper internal auth headers or pass allowLocal where the API supports it"],"exampleFix":"// before\nawait permission.assertWorkspace({ userId, workspaceId, action: 'Doc.Write' });\n\n// after\nconst allowed = await permission.canWorkspace({ userId, workspaceId, action: 'Doc.Write' });\nif (!allowed) {\n  return respondWithSignInOrRequestAccess(workspaceId); // graceful denial instead of thrown error\n}\nawait doWorkspaceWrite();","handlingStrategy":"validation","validationCode":"const allowed = await permission.canWorkspace({ userId, workspaceId, action });\nif (!allowed) {\n  return respondAccessDenied(workspaceId); // redirect to sign-in / request access\n}\nawait performWorkspaceAction(workspaceId);","typeGuard":"function isSpaceAccessDenied(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } }).extensions?.code === 'space_access_denied';\n}","tryCatchPattern":"try {\n  await workspaceAction(workspaceId);\n} catch (e) {\n  if (isSpaceAccessDenied(e)) {\n    return handleNoMembership(e.extensions.spaceId); // re-auth, request access, or drop workspace locally\n  }\n  throw e;\n}","preventionTips":["Prefer canWorkspace checks (boolean) over assertWorkspace when you want graceful degradation","Re-validate membership after role changes and token refreshes","Pass proper auth context on every workspace-scoped request; anonymous (undefined userId) is always denied"],"tags":["permission","workspace","authorization","access-control"],"backgroundTag":"permission-denied","analyzedSha":"591f874dad30887a80143a061a44bd3ca7ee3299","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}