{"record":{"id":"88842f78c302f514","repo":"toeverything/AFFiNE","slug":"cannot-grant-owner-role-of-a-workspace-to-a-user","errorCode":null,"errorMessage":"Cannot grant Owner role of a workspace to a user.","messagePattern":"Cannot grant Owner role of a workspace to a user\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/backend/server/src/models/permission-write.ts","lineNumber":188,"sourceCode":"      create: {\n        workspaceId,\n        userId,\n        role: 'owner',\n        state: 'active',\n        source: 'legacy',\n      },\n    });\n  }\n\n  @Transactional()\n  async setActive(\n    workspaceId: string,\n    userId: string,\n    role: WorkspaceRole,\n    data: { source?: PermissionSource } = {}\n  ) {\n    if (role === WorkspaceRole.Owner) {\n      throw new Error('Cannot grant Owner role of a workspace to a user.');\n    }\n\n    const invitation = await this.db.workspaceInvitation.findUnique({\n      where: {\n        workspaceId_inviteeUserId: {\n          workspaceId,\n          inviteeUserId: userId,\n        },\n      },\n    });\n    await this.db.workspaceInvitation.deleteMany({\n      where: { workspaceId, inviteeUserId: userId },\n    });\n\n    return await this.db.workspaceMember.upsert({\n      where: {\n        workspaceId_userId_state: {\n          workspaceId,","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/permission-write.ts#L170-L206","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Route Owner grants through WorkspaceMemberModel.setOwner(workspaceId, userId, fallbackRole), not setActive.","Special-case Owner in the UI/controller: if (role === WorkspaceRole.Owner) return setOwner(...).","Filter Owner out of any bulk setActive loop.","Add a type/unit test asserting setActive rejects Owner so regressions surface early."],"exampleFix":"// before\nawait memberModel.setActive(workspaceId, userId, WorkspaceRole.Owner);\n// after\nif (role === WorkspaceRole.Owner) {\n  await memberModel.setOwner(workspaceId, userId, WorkspaceRole.Admin);\n} else {\n  await memberModel.setActive(workspaceId, userId, role);\n}","handlingStrategy":"validation","validationCode":"if (role === WorkspaceRole.Owner) {\n  throw new Error('Use WorkspaceMemberModel.setOwner to grant Owner, not setActive');\n}\nawait memberModel.setActive(workspaceId, userId, role);","typeGuard":"const isActivationRole = (r: WorkspaceRole): boolean =>\n  r !== WorkspaceRole.Owner;","tryCatchPattern":"try {\n  await memberModel.setActive(workspaceId, userId, role);\n} catch (e) {\n  if (e instanceof Error && /Cannot grant Owner/i.test(e.message)) {\n    await memberModel.setOwner(workspaceId, userId, WorkspaceRole.Admin);\n    return;\n  }\n  throw e;\n}","preventionTips":["Special-case Owner in the controller: route to setOwner, not setActive.","Filter Owner out of bulk setActive loops.","Add a unit test asserting setActive rejects Owner."],"tags":["workspace","permission","owner","api-contract"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}