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

  1. 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
  2. On this error, prompt the author to invite the user or grant read access, then retry the mention
  3. Refresh the mention candidate list when doc visibility/roles change
  4. 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

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


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)