toeverything/AFFiNE · error · SpaceAccessDenied

space_access_denied

space_access_denied

Error message

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

What it means

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.

Source

Thrown at packages/backend/server/src/plugins/indexer/service.ts:160

        )
      );
      for (const doc of docs) {
        if (!doc.title) doc.title = titles.get(doc.docId) ?? '';
      }
    }
    const users = await this.models.user.getPublicUsersMap(userIds);
    for (const doc of docs) {
      doc.createdByUser = users.get(doc.createdByUserId);
      doc.updatedByUser = users.get(doc.updatedByUserId);
    }
    return docs;
  }

  private unwrap<T>(output: SearchOperationOutput, workspaceId: string): T {
    if (output.ok) return output.value as T;
    switch (output.errorCode) {
      case 'workspace_denied':
        throw new SpaceAccessDenied({ spaceId: workspaceId });
      case 'invalid_request':
      case 'unsupported_query':
        throw new InvalidIndexerInput({ reason: output.errorCode });
      case 'provider_unavailable':
        throw new SearchProviderUnavailable();
      case 'index_not_ready':
        throw new SearchIndexNotReady({ spaceId: workspaceId });
      case 'permission_syncing':
        throw new SearchPermissionSyncing();
      case 'index_failed':
        throw new SearchIndexFailed({
          diagnosticId: 'search_workspace_reconcile_failed',
        });
      default:
        throw new InternalServerError();
    }
  }
}

View on GitHub (pinned to b6de0ad51b)

Solutions

  1. Verify the caller's membership/permission for workspaceId before invoking search
  2. Refresh the session/token and retry to rule out a stale auth context
  3. Check that workspaceId in the query is actually the workspace the client has open

Example fix

// before
const docs = await indexerService.search(user, { workspaceId, query });
// after: gate on permission first
const allowed = await permissionService.tryCheck(workspaceId, user.id);
if (!allowed) throw new SpaceAccessDenied({ spaceId: workspaceId });
const docs = await indexerService.search(user, { workspaceId, query });
Defensive patterns

Strategy: validation

Validate before calling

// check membership before calling indexer search
const permission = await permissionService.get(workspaceId, user.id);
if (!permission || !permission.canRead) {
  return forbidden(spaceId);
}
const docs = await indexerService.search(user, { workspaceId, query });

Try / catch

// distinguish denial from provider/permission availability
try {
  return await indexerService.search(user, { workspaceId, query });
} catch (e) {
  if (e instanceof SpaceAccessDenied) { // code === 'space_access_denied'
    return fortyThree({ spaceId: e.spaceId }); // 403, do not retry
  }
  throw e; // provider/permission-unavailable handled separately
}

Prevention

When it happens

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

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

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b6de0ad51b (2026-08-21). Data as JSON: /api/errors/f95ee2e1dfd718df. Report an issue: GitHub.