toeverything/AFFiNE · error · SpaceAccessDenied

space_access_denied

space_access_denied

Error message

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

What it means

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.

Solutions

  1. Verify the user's membership/role for the workspace before making the call (canWorkspace instead of assertWorkspace)
  2. Re-authenticate and refresh the session if membership changed recently
  3. Request the needed role from the workspace owner, or use an account with sufficient permission
  4. For internal/service callers, send the proper internal auth headers or pass allowLocal where the API supports it

Example fix

// before
await permission.assertWorkspace({ userId, workspaceId, action: 'Doc.Write' });

// after
const allowed = await permission.canWorkspace({ userId, workspaceId, action: 'Doc.Write' });
if (!allowed) {
  return respondWithSignInOrRequestAccess(workspaceId); // graceful denial instead of thrown error
}
await doWorkspaceWrite();
Defensive patterns

Strategy: validation

Validate before calling

const allowed = await permission.canWorkspace({ userId, workspaceId, action });
if (!allowed) {
  return respondAccessDenied(workspaceId); // redirect to sign-in / request access
}
await performWorkspaceAction(workspaceId);

Type guard

function isSpaceAccessDenied(e: unknown): boolean {
  return (e as { extensions?: { code?: string } }).extensions?.code === 'space_access_denied';
}

Try / catch

try {
  await workspaceAction(workspaceId);
} catch (e) {
  if (isSpaceAccessDenied(e)) {
    return handleNoMembership(e.extensions.spaceId); // re-auth, request access, or drop workspace locally
  }
  throw e;
}

Prevention

When it happens

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

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

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@591f874dad (2026-08-18). Data as JSON: /api/errors/9e63b5f9bfeb0584. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/permission/service.ts:110

    workspaceId: string;
    action: PermissionWorkspaceAction;
    allowLocal?: boolean;
  }) {
    const output = await this.workspacePermissions({
      ...input,
      actions: [input.action],
    });
    return output.decisions[0]?.allowed ?? false;
  }

  async assertWorkspace(input: {
    userId?: string;
    workspaceId: string;
    action: PermissionWorkspaceAction;
    allowLocal?: boolean;
  }) {
    if (!(await this.canWorkspace(input))) {
      throw new SpaceAccessDenied({ spaceId: input.workspaceId });
    }
  }

  async docPermissions(input: {
    userId?: string;
    workspaceId: string;
    docId: string;
    actions: PermissionDocAction[];
    allowLocal?: boolean;
  }) {
    const output = await this.evaluateLoaded({
      userId: input.userId,
      workspaceId: input.workspaceId,
      docs: [{ docId: input.docId, actions: input.actions }],
      allowLocal: input.allowLocal,
    });
    const doc = output.docs[0];
    return {

View on GitHub (pinned to 591f874dad)