{"record":{"id":"2043742a94668548","repo":"toeverything/AFFiNE","slug":"can-not-batch-grant-doc-owner-permissions-204374","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/permission-write.ts","lineNumber":582,"sourceCode":"      create: {\n        workspaceId,\n        docId,\n        principalType: 'user',\n        principalId: userId,\n        role: docRoleToNew(role),\n      },\n    });\n  }\n\n  @Transactional()\n  async batchSetUserRoles(\n    workspaceId: string,\n    docId: string,\n    userIds: string[],\n    role: DocRole\n  ) {\n    if (role === DocRole.Owner) {\n      throw new CanNotBatchGrantDocOwnerPermissions();\n    }\n    if (userIds.length === 0) {\n      return 0;\n    }\n\n    const grantRole = docRoleToNew(role);\n    for (const userId of userIds) {\n      await this.db.docGrant.upsert({\n        where: {\n          workspaceId_docId_principalType_principalId: {\n            workspaceId,\n            docId,\n            principalType: 'user',\n            principalId: userId,\n          },\n        },\n        update: {\n          role: grantRole,","sourceCodeStart":564,"sourceCodeEnd":600,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/models/permission-write.ts#L564-L600","documentation":"CanNotBatchGrantDocOwnerPermissions, thrown by PermissionWriteModel.batchSetUserRoles (permission-write.ts:577-588) - the permission write path enforces the same rule as the doc-user model: DocRole.Owner cannot be batch granted. Here the role guard runs before the empty-list early return, so even userIds = [] with role Owner throws. Other roles are converted with docRoleToNew(role) and upserted per user into docGrant.","triggerScenarios":"batchSetUserRoles(workspaceId, docId, userIds, DocRole.Owner) with any userIds value, including an empty array, since the Owner check precedes the length check in this model.","commonSituations":"Role dropdowns that include Owner; migrating permission data by mapping an 'owner' flag straight into a batch grant call.","solutions":["Route ownership changes through the owner-transfer flow instead of batch grants","Grant Admin/Write/Read in batch - only Owner is refused","Validate the role at the API boundary so callers never reach this throw"],"exampleFix":"// before\nawait permissions.batchSetUserRoles(workspaceId, docId, userIds, role);\n\n// after\nif (role === DocRole.Owner) {\n  throw new Error('Owner cannot be batch granted; use the owner-transfer flow');\n}\nawait permissions.batchSetUserRoles(workspaceId, docId, userIds, role);","handlingStrategy":"validation","validationCode":"if (role === DocRole.Owner) {\n  throw new Error('Owner cannot be batch granted; use the owner-transfer flow');\n}\nawait permissions.batchSetUserRoles(workspaceId, docId, userIds, role);","typeGuard":"const isBatchGrantableRole = (r: DocRole): boolean => r !== DocRole.Owner;","tryCatchPattern":"try {\n  await permissions.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":["Validate the role before calling - even empty batches throw for Owner in this model","Map ownership changes to the dedicated transfer endpoint","Keep role whitelists in one shared module used by every permission API"],"tags":["permission","role","owner","doc","validation"],"backgroundTag":"cannot-grant-owner-role","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}