toeverything/AFFiNE · error · MentionUserDocAccessDenied
mention_user_doc_access_denied
mention_user_doc_access_denied
Error message
Mentioned user can not access doc ${docId}. What it means
After asserting the caller has Doc.Update on the doc, createMention checks that the mentioned user has Doc.Read on the same doc; if the access-control evaluation returns false it throws MentionUserDocAccessDenied (no_permission / mention_user_doc_access_denied) with the docId. Mentioning someone who cannot open the doc would create a notification pointing at forbidden content, so it is blocked.
Solutions
- Before sending, check the mentioned user's access (same ac.user(...).doc(...).can('Doc.Read') evaluation) or filter mention candidates to users with doc read access
- On this error, prompt the author to invite the user or grant read access, then retry the mention
- Refresh the mention candidate list when doc visibility/roles change
- Ensure the mentioned userId is an active member of the workspace at all
Example fix
// before
await createMention({ userId, body: { workspaceId, doc, createdByUserId: me.id } });
// after
const canRead = await ac.user(userId).doc(workspaceId, doc.id).can('Doc.Read');
if (!canRead) {
notifyAuthor(`${userName} cannot read this doc; grant access first`);
} else {
await createMention({ userId, body: { workspaceId, doc, createdByUserId: me.id } });
} Defensive patterns
Strategy: validation
Validate before calling
const canRead = await ac
.user(targetUserId)
.doc(workspaceId, doc.id)
.can('Doc.Read');
if (!canRead) {
return promptGrantAccess(targetUserId, doc.id); // invite or pick someone else
}
await createMention(input); Type guard
function isMentionDocAccessDenied(e: unknown): boolean {
return (e as { extensions?: { code?: string } }).extensions?.code === 'mention_user_doc_access_denied';
} Try / catch
try {
await createMention(input);
} catch (e) {
if (isMentionDocAccessDenied(e)) {
return showInvitePrompt(input.userId, e.extensions.docId); // recoverable: grant read, then retry
}
throw e;
} Prevention
- Filter mention candidates by doc read permission, not just workspace membership
- Re-check permissions when doc visibility changes (public → private)
- Offer an inline 'invite to doc' action when this error fires
When it happens
Trigger: Mentioning a user who is not a member of the workspace; mentioning a member whose doc-level role does not include read on a private/restricted doc; the doc's visibility was changed after the mention picker loaded its user list.
Common situations: Mention pickers listing all workspace members regardless of per-doc permissions; mentioning external collaborators on internal docs; stale permission caches client-side after roles were revoked.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- mention_user_oneself_denied
- doc_action_denied
- space_access_denied
- access_denied
- can_not_batch_grant_doc_owner_permissions
AI-assisted analysis of toeverything/AFFiNE@591f874dad (2026-08-18).
Data as JSON: /api/errors/3a0a8be37de56e3b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/notification/resolver.ts:130
createdByUserId: me.id,
},
});
if (parsedInput.userId === me.id) {
throw new MentionUserOneselfDenied();
}
// currentUser can update the doc
await this.ac
.user(me.id)
.doc(parsedInput.body.workspaceId, parsedInput.body.doc.id)
.assert('Doc.Update');
// mention user can read the doc
if (
!(await this.ac
.user(parsedInput.userId)
.doc(parsedInput.body.workspaceId, parsedInput.body.doc.id)
.can('Doc.Read'))
) {
throw new MentionUserDocAccessDenied({
docId: parsedInput.body.doc.id,
});
}
const notification = await this.service.createMention(parsedInput);
return notification.id;
}
@Mutation(() => Boolean, {
description: 'mark notification as read',
})
async readNotification(
@CurrentUser() me: UserType,
@Args('id') notificationId: string
) {
await this.service.markAsRead(me.id, notificationId);
return true;
}
View on GitHub (pinned to 591f874dad)