toeverything/AFFiNE · error · CanNotBatchGrantDocOwnerPermissions
can_not_batch_grant_doc_owner_permissions
can_not_batch_grant_doc_owner_permissions
Error message
Can not batch grant doc owner permissions.
What it means
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.
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
Example fix
// before - role comes straight from the request
await docUser.batchSetUserRoles(workspaceId, docId, userIds, role);
// after - reject Owner at the boundary with a clear message
if (role === DocRole.Owner) {
throw new Error('Owner cannot be batch granted; use the owner-transfer flow');
}
await docUser.batchSetUserRoles(workspaceId, docId, userIds, role); Defensive patterns
Strategy: validation
Validate before calling
const BATCH_GRANTABLE = new Set([DocRole.Admin, DocRole.Write, DocRole.Read]);
if (!BATCH_GRANTABLE.has(role)) {
throw new Error(`Role ${role} cannot be batch granted; use owner transfer`);
}
await docUser.batchSetUserRoles(workspaceId, docId, userIds, role); Type guard
const isBatchGrantableRole = (r: DocRole): boolean => r !== DocRole.Owner;
Try / catch
try {
await docUser.batchSetUserRoles(workspaceId, docId, userIds, role);
} catch (e) {
if (e instanceof CanNotBatchGrantDocOwnerPermissions) {
// route to the owner-transfer flow instead
}
throw e;
} Prevention
- 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)
When it happens
Trigger: batchSetUserRoles(workspaceId, docId, userIds, DocRole.Owner) with a non-empty userIds array - the Owner guard fires after the length check.
Common situations: Permission-management UI passing the selected role straight through, including Owner; import scripts trying to set many owners at once.
Related errors
- can_not_batch_grant_doc_owner_permissions
- -32001
- doc_action_denied
- doc_default_role_can_not_be_owner
- expect_to_grant_doc_user_roles
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/bbc7d824b45eb15b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/models/doc-user.ts:67
assert(role !== DocRole.Owner, 'Cannot set Owner role of a doc to a user.');
await this.models.docGrant.set(workspaceId, docId, userId, role);
return await this.get(workspaceId, docId, userId);
}
@Transactional()
async batchSetUserRoles(
workspaceId: string,
docId: string,
userIds: string[],
role: DocRole
) {
if (userIds.length === 0) {
return 0;
}
if (role === DocRole.Owner) {
throw new CanNotBatchGrantDocOwnerPermissions();
}
return await this.models.docGrant.batchSetUserRoles(
workspaceId,
docId,
userIds,
role
);
}
@Transactional()
async delete(workspaceId: string, docId: string, userId: string) {
await this.models.docGrant.delete(workspaceId, docId, userId);
}
@Transactional()
async deleteByUserId(userId: string) {
await this.db.docGrant.deleteMany({View on GitHub (pinned to b4c8548c09)