{"record":{"id":"bbc7d824b45eb15b","repo":"toeverything/AFFiNE","slug":"can-not-batch-grant-doc-owner-permissions","errorCode":"can_not_batch_grant_doc_owner_permissions","errorMessage":"Can not batch grant doc owner permissions.","messagePattern":"Can not batch grant doc owner permissions\\.","errorType":"exception","errorClass":"CanNotBatchGrantDocOwnerPermissions","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/models/doc-user.ts","lineNumber":67,"sourceCode":"    assert(role !== DocRole.Owner, 'Cannot set Owner role of a doc to a user.');\n\n    await this.models.docGrant.set(workspaceId, docId, userId, role);\n    return await this.get(workspaceId, docId, userId);\n  }\n\n  @Transactional()\n  async batchSetUserRoles(\n    workspaceId: string,\n    docId: string,\n    userIds: string[],\n    role: DocRole\n  ) {\n    if (userIds.length === 0) {\n      return 0;\n    }\n\n    if (role === DocRole.Owner) {\n      throw new CanNotBatchGrantDocOwnerPermissions();\n    }\n\n    return await this.models.docGrant.batchSetUserRoles(\n      workspaceId,\n      docId,\n      userIds,\n      role\n    );\n  }\n\n  @Transactional()\n  async delete(workspaceId: string, docId: string, userId: string) {\n    await this.models.docGrant.delete(workspaceId, docId, userId);\n  }\n\n  @Transactional()\n  async deleteByUserId(userId: string) {\n    await this.db.docGrant.deleteMany({","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/models/doc-user.ts#L49-L85","documentation":"CanNotBatchGrantDocOwnerPermissions, thrown by DocUserModel.batchSetUserRoles (doc-user.ts:60-70). Doc ownership is singular and transferable, not a grantable role, so batch-assigning DocRole.Owner to a list of users is rejected up front; every other role is delegated to docGrant.batchSetUserRoles. The empty-list early return runs first, so userIds = [] never throws.","triggerScenarios":"batchSetUserRoles(workspaceId, docId, userIds, DocRole.Owner) with a non-empty userIds array - the Owner guard fires after the length check.","commonSituations":"Permission-management UI passing the selected role straight through, including Owner; import scripts trying to set many owners at once.","solutions":["Route ownership changes through the dedicated owner-transfer flow (one user at a time)","Grant Admin/Write/Read in batch operations - Owner is the only rejected role","Filter Owner out of role pickers on batch-grant screens"],"exampleFix":"// before - role comes straight from the request\nawait docUser.batchSetUserRoles(workspaceId, docId, userIds, role);\n\n// after - reject Owner at the boundary with a clear message\nif (role === DocRole.Owner) {\n  throw new Error('Owner cannot be batch granted; use the owner-transfer flow');\n}\nawait docUser.batchSetUserRoles(workspaceId, docId, userIds, role);","handlingStrategy":"validation","validationCode":"const BATCH_GRANTABLE = new Set([DocRole.Admin, DocRole.Write, DocRole.Read]);\nif (!BATCH_GRANTABLE.has(role)) {\n  throw new Error(`Role ${role} cannot be batch granted; use owner transfer`);\n}\nawait docUser.batchSetUserRoles(workspaceId, docId, userIds, role);","typeGuard":"const isBatchGrantableRole = (r: DocRole): boolean => r !== DocRole.Owner;","tryCatchPattern":"try {\n  await docUser.batchSetUserRoles(workspaceId, docId, userIds, role);\n} catch (e) {\n  if (e instanceof CanNotBatchGrantDocOwnerPermissions) {\n    // route to the owner-transfer flow instead\n  }\n  throw e;\n}","preventionTips":["Exclude Owner from batch role pickers","Treat ownership as a transfer operation, never a grant","Skip the call entirely for empty userIds (it returns 0)"],"tags":["doc","permission","role","owner","validation"],"backgroundTag":"cannot-grant-owner-role","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}