toeverything/AFFiNE · error · DocActionDenied

doc_action_denied

doc_action_denied

Error message

You do not have permission to perform ${action} action on doc ${docId}.

What it means

Thrown by PermissionService.assertDoc (packages/backend/server/src/core/permission/service.ts:171) when canDoc resolves the user's permission rules for the doc and returns allowed=false. It is the central doc-level authorization gate: any doc API (read/update/delete/move) wrapped with assertDoc rejects callers whose effective rules do not include the requested PermissionDocAction.

Solutions

  1. Verify the user has an active workspace membership (workspaceUser.getActive) for the doc's workspace.
  2. Pre-check with the non-throwing PermissionService.canDoc({ userId, workspaceId, docId, action }) before performing the call.
  3. Filter doc lists through filterReadableDocs instead of asserting per doc.
  4. For local workspaces pass allowLocal: true where the call supports it.

Example fix

// before
await docService.update({ userId, workspaceId, docId, data }); // may throw doc_action_denied

// after
const allowed = await permission.canDoc({ userId, workspaceId, docId, action: 'Doc.Update' });
if (!allowed) throw new Forbidden('no access');
await docService.update({ userId, workspaceId, docId, data });
Defensive patterns

Strategy: validation

Validate before calling

const allowed = await permission.canDoc({
  userId,
  workspaceId,
  docId,
  action: 'Doc.Update',
});
if (!allowed) {
  return res.status(403).json({ code: 'doc_action_denied' });
}

Type guard

const isDocActionDenied = (e: unknown): e is DocActionDenied =>
  e instanceof DocActionDenied;

Try / catch

try {
  await permission.assertDoc({ userId, workspaceId, docId, action });
} catch (e) {
  if (e instanceof DocActionDenied) return res.status(403).json({ code: e.code });
  throw e;
}

Prevention

When it happens

Trigger: Calling a doc route that asserts a doc action while the user is not an active member of the workspace, is a collaborator whose role lacks that action, or the doc is in a state (e.g. trashed) where the rule does not grant it; also passing a userId that does not match the session owner.

Common situations: Stale token from a removed member, custom integrations calling doc APIs without joining the workspace first, forgetting allowLocal for local workspaces, or a wrong workspaceId/docId pairing.

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/9658ca6b08ce0f06. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/permission/service.ts:156

    action: PermissionDocAction;
    allowLocal?: boolean;
  }) {
    const output = await this.docPermissions({
      ...input,
      actions: [input.action],
    });
    return output.decisions[0]?.allowed ?? false;
  }

  async assertDoc(input: {
    userId?: string;
    workspaceId: string;
    docId: string;
    action: PermissionDocAction;
    allowLocal?: boolean;
  }) {
    if (!(await this.canDoc(input))) {
      throw new DocActionDenied({
        action: input.action,
        docId: input.docId,
        spaceId: input.workspaceId,
      });
    }
  }

  async filterReadableDocs<T extends { docId: string }>(input: {
    userId?: string;
    workspaceId: string;
    docs: T[];
    allowLocal?: boolean;
  }) {
    const decisions = await this.batchDocPermissions({
      ...input,
      docs: input.docs.map(doc => ({
        docId: doc.docId,
        actions: ['Doc.Read'],

View on GitHub (pinned to 591f874dad)