toeverything/AFFiNE · error · Error

Cannot grant Owner role of a workspace to a user.

Error message

Cannot grant Owner role of a workspace to a user.

What it means

Plain Error thrown by WorkspaceMemberModel.setActive (packages/backend/server/src/models/permission-write.ts:188). setActive is the path for activating/inviting members, and it explicitly forbids granting WorkspaceRole.Owner — ownership is a transfer, not an activation. Any call with role === WorkspaceRole.Owner throws immediately before any invitation/member write. Because it is a plain Error, callers see internal_server_error unless they validate first.

Source

Thrown at packages/backend/server/src/models/permission-write.ts:188

      create: {
        workspaceId,
        userId,
        role: 'owner',
        state: 'active',
        source: 'legacy',
      },
    });
  }

  @Transactional()
  async setActive(
    workspaceId: string,
    userId: string,
    role: WorkspaceRole,
    data: { source?: PermissionSource } = {}
  ) {
    if (role === WorkspaceRole.Owner) {
      throw new Error('Cannot grant Owner role of a workspace to a user.');
    }

    const invitation = await this.db.workspaceInvitation.findUnique({
      where: {
        workspaceId_inviteeUserId: {
          workspaceId,
          inviteeUserId: userId,
        },
      },
    });
    await this.db.workspaceInvitation.deleteMany({
      where: { workspaceId, inviteeUserId: userId },
    });

    return await this.db.workspaceMember.upsert({
      where: {
        workspaceId_userId_state: {
          workspaceId,

View on GitHub (pinned to 26c515e050)

Solutions

  1. Route Owner grants through WorkspaceMemberModel.setOwner(workspaceId, userId, fallbackRole), not setActive.
  2. Special-case Owner in the UI/controller: if (role === WorkspaceRole.Owner) return setOwner(...).
  3. Filter Owner out of any bulk setActive loop.
  4. Add a type/unit test asserting setActive rejects Owner so regressions surface early.

Example fix

// before
await memberModel.setActive(workspaceId, userId, WorkspaceRole.Owner);
// after
if (role === WorkspaceRole.Owner) {
  await memberModel.setOwner(workspaceId, userId, WorkspaceRole.Admin);
} else {
  await memberModel.setActive(workspaceId, userId, role);
}
Defensive patterns

Strategy: validation

Validate before calling

if (role === WorkspaceRole.Owner) {
  throw new Error('Use WorkspaceMemberModel.setOwner to grant Owner, not setActive');
}
await memberModel.setActive(workspaceId, userId, role);

Type guard

const isActivationRole = (r: WorkspaceRole): boolean =>
  r !== WorkspaceRole.Owner;

Try / catch

try {
  await memberModel.setActive(workspaceId, userId, role);
} catch (e) {
  if (e instanceof Error && /Cannot grant Owner/i.test(e.message)) {
    await memberModel.setOwner(workspaceId, userId, WorkspaceRole.Admin);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling setActive(workspaceId, userId, WorkspaceRole.Owner, data) — the very first statement in setActive is the equality check, so any Owner role input throws. This is an API-contract violation: callers must use setOwner to grant ownership.

Common situations: Permission matrix UI mapping 'Owner' through the same setActive call as other roles; bulk role import that includes Owner; client code that switches on role without special-casing Owner; refactor that merged the owner path into setActive by mistake.

Related errors


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