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 by the deprecated `workspaceRolePermissions` query when `ac.user(user.id).workspace(id).permissions()` returns no role for the caller — i.e. the user has no membership or role assignment in that workspace. Coded `space_access_denied` (no_permission) with `{ spaceId }`.

Source

Thrown at packages/backend/server/src/core/workspaces/resolvers/workspace.ts:196

    return workspace;
  }

  @Query(() => WorkspaceRolePermissions, {
    description: 'Get workspace role permissions',
    deprecationReason: 'use WorkspaceType[permissions] instead',
  })
  async workspaceRolePermissions(
    @CurrentUser() user: CurrentUser,
    @Args('id') id: string
  ): Promise<WorkspaceRolePermissions> {
    const { role, permissions } = await this.ac
      .user(user.id)
      .workspace(id)
      .permissions();

    if (!role) {
      throw new SpaceAccessDenied({ spaceId: id });
    }

    return {
      role,
      permissions: mapPermissionsToGraphqlPermissions(permissions),
    };
  }

  @Mutation(() => WorkspaceType, {
    description: 'Create a new workspace',
  })
  async createWorkspace(
    @CurrentUser() user: CurrentUser,
    // we no longer support init workspace with a preload file
    // use sync system to uploading them once created
    @Args({ name: 'init', type: () => GraphQLUpload, nullable: true })
    init: FileUpload | null
  ) {

View on GitHub (pinned to 26c515e050)

Solutions

  1. Confirm the user is still a member of the workspace before querying role permissions.
  2. Prefer the non-deprecated `WorkspaceType[permissions]` field noted in the deprecation reason.
  3. Handle `space_access_denied` by redirecting to the workspace list or a 'no access' page.
  4. Refresh the session/membership cache if the role was just granted and is not yet visible.

Example fix

// before
const { role, permissions } = await sdk.workspaceRolePermissions({ id });

// after
const ws = await sdk.workspace({ id }); // WorkspaceType.permissions is the replacement
const permissions = ws.permissions;

// guard:
try { ... } catch (e) {
  if (e.code === 'space_access_denied') router.push('/workspaces');
}
Defensive patterns

Strategy: validation

Validate before calling

// Prefer the non-deprecated path and confirm membership
if (!myWorkspaces.find(w => w.id === workspaceId)) {
  router.push('/workspaces');
  return;
}
const ws = await sdk.workspace({ id: workspaceId });
const permissions = ws.permissions;

Type guard

function hasWorkspaceAccess(workspaceId, myWorkspaces) {
  return myWorkspaces.some(w => w.id === workspaceId);
}

Try / catch

try {
  const { role, permissions } = await sdk.workspaceRolePermissions({ id });
} catch (e) {
  if (e.code === 'space_access_denied') {
    router.push('/workspaces');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `workspaceRolePermissions(workspaceId)` as a user who is not a member of the workspace — no role row in `workspace_user`, no inherited permission. Also fires after a user is removed but their client still issues the query.

Common situations: User removed from the workspace but the page still mounted; a shared URL opened by an outsider; permission system hasn't yet propagated a freshly-revoked role; querying a workspace the user was never invited to.

Related errors


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