{"record":{"id":"455ab47be60d4e40","repo":"toeverything/AFFiNE","slug":"space-access-denied-455ab4","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/workspaces/controller.ts","lineNumber":117,"sourceCode":"  // NOTE: because graphql can't represent a File, so we have to use REST API to get blob\n  @Public()\n  @Get('/:id/blobs/:name')\n  @CallMetric('controllers', 'workspace_get_blob')\n  async blob(\n    @CurrentUser() user: CurrentUser | undefined,\n    @Param('id') workspaceId: string,\n    @Param('name') name: string,\n    @Query('redirect') redirect: string | undefined,\n    @Res() res: Response\n  ) {\n    const canReadWorkspace = await this.ac\n      .user(user?.id ?? 'anonymous')\n      .workspace(workspaceId)\n      .can('Workspace.Read');\n    const canReadSharedWorkspaceBlobs =\n      await this.canReadSharedWorkspaceBlobs(workspaceId);\n    if (!canReadWorkspace && !canReadSharedWorkspaceBlobs) {\n      throw new SpaceAccessDenied({ spaceId: workspaceId });\n    }\n    const { body, metadata, redirectUrl } = await this.storage.get(\n      workspaceId,\n      name,\n      true\n    );\n\n    if (redirectUrl) {\n      // redirect to signed url\n      if (redirect === 'manual') {\n        return res.send({\n          url: redirectUrl,\n        });\n      } else {\n        return res.redirect(redirectUrl);\n      }\n    }\n","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/controller.ts#L99-L135","documentation":"Thrown when fetching a workspace blob: the requester fails BOTH the Workspace.Read access check AND canReadSharedWorkspaceBlobs. The user is neither a workspace member nor covered by a public/share grant that allows reading blobs.","triggerScenarios":"An anonymous or unauthenticated request (or a user from another workspace) hits /workspaces/:id/blobs/:name without any share grant permitting blob reads.","commonSituations":"Direct/hotlinked blob URL with no credentials; share token expired; workspace switched from public to private; wrong cookie/session.","solutions":["Authenticate the request and ensure the user is a member with Workspace.Read.","Re-enable public/share access for the blob if it should be shared.","Provide a valid share token / signed URL if using link sharing.","Remove dead hotlinks that assume public access."],"exampleFix":"// before\nfetch(`/api/workspaces/${ws}/blobs/${name}`) // no creds\n// after\nfetch(`/api/workspaces/${ws}/blobs/${name}`, { credentials: 'include' })","handlingStrategy":"validation","validationCode":"const canWs = await ac.user(userId ?? 'anonymous').workspace(ws).can('Workspace.Read')\nconst canShared = await canReadSharedWorkspaceBlobs(ws)\nif (!canWs && !canShared) return redirectToLogin()","typeGuard":null,"tryCatchPattern":"try { await getBlob(ws, name) } catch (e) {\n  if (e.code === 'space_access_denied') redirectToLogin()\n  else throw e\n}","preventionTips":["Send credentials on blob requests.","Use short-lived signed URLs for sharing.","Audit public-blob toggles."],"tags":["authz","access-control","blobs","permissions"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}