toeverything/AFFiNE · error · SpaceAccessDenied

space_access_denied

space_access_denied

Error message

You do not have permission to access Space ${spaceId}.

What it means

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.

Source

Thrown at packages/backend/server/src/core/workspaces/controller.ts:117

  // NOTE: because graphql can't represent a File, so we have to use REST API to get blob
  @Public()
  @Get('/:id/blobs/:name')
  @CallMetric('controllers', 'workspace_get_blob')
  async blob(
    @CurrentUser() user: CurrentUser | undefined,
    @Param('id') workspaceId: string,
    @Param('name') name: string,
    @Query('redirect') redirect: string | undefined,
    @Res() res: Response
  ) {
    const canReadWorkspace = await this.ac
      .user(user?.id ?? 'anonymous')
      .workspace(workspaceId)
      .can('Workspace.Read');
    const canReadSharedWorkspaceBlobs =
      await this.canReadSharedWorkspaceBlobs(workspaceId);
    if (!canReadWorkspace && !canReadSharedWorkspaceBlobs) {
      throw new SpaceAccessDenied({ spaceId: workspaceId });
    }
    const { body, metadata, redirectUrl } = await this.storage.get(
      workspaceId,
      name,
      true
    );

    if (redirectUrl) {
      // redirect to signed url
      if (redirect === 'manual') {
        return res.send({
          url: redirectUrl,
        });
      } else {
        return res.redirect(redirectUrl);
      }
    }

View on GitHub (pinned to 26c515e050)

Solutions

  1. Authenticate the request and ensure the user is a member with Workspace.Read.
  2. Re-enable public/share access for the blob if it should be shared.
  3. Provide a valid share token / signed URL if using link sharing.
  4. Remove dead hotlinks that assume public access.

Example fix

// before
fetch(`/api/workspaces/${ws}/blobs/${name}`) // no creds
// after
fetch(`/api/workspaces/${ws}/blobs/${name}`, { credentials: 'include' })
Defensive patterns

Strategy: validation

Validate before calling

const canWs = await ac.user(userId ?? 'anonymous').workspace(ws).can('Workspace.Read')
const canShared = await canReadSharedWorkspaceBlobs(ws)
if (!canWs && !canShared) return redirectToLogin()

Try / catch

try { await getBlob(ws, name) } catch (e) {
  if (e.code === 'space_access_denied') redirectToLogin()
  else throw e
}

Prevention

When it happens

Trigger: An anonymous or unauthenticated request (or a user from another workspace) hits /workspaces/:id/blobs/:name without any share grant permitting blob reads.

Common situations: Direct/hotlinked blob URL with no credentials; share token expired; workspace switched from public to private; wrong cookie/session.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/455ab47be60d4e40. Report an issue: GitHub.