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 when canDoc() evaluates the doc-level permission rule and returns allowed=false. Distinct from workspace access: the user may be a workspace member but lacks the specific doc action (Doc.Read, Doc.Update, Doc.Comments.Create, etc.). Carries docId, action, and spaceId in the payload for client display.

Source

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

    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 26c515e050)

Solutions

  1. Check the doc access matrix with docPermissions() before issuing the mutating call.
  2. Ensure the user's workspace role grants the underlying action and no doc-level override revokes it.
  3. If using a custom permission policy, confirm the rule set includes the requested action for the role.
  4. For anonymous/public flows, verify the doc is actually published before asserting Doc.Read.

Example fix

// before
await perms.assertDoc({ userId, workspaceId, docId, action: 'Doc.Update' });

// after
const decisions = await perms.docPermissions({ userId, workspaceId, docId, actions: ['Doc.Update'] });
if (!decisions.decisions[0]?.allowed) {
  throw new DocActionDenied({ action: 'Doc.Update', docId, spaceId: workspaceId });
}
Defensive patterns

Strategy: validation

Validate before calling

const { decisions } = await perms.docPermissions({ userId, workspaceId, docId, actions: [action] });
if (!decisions[0]?.allowed) {
  return { denied: true, docId, action };
}

Type guard

function isDocAction(a: string): a is PermissionDocAction {
  return ['Doc.Read','Doc.Duplicate','Doc.Trash','Doc.Restore','Doc.Delete','Doc.Update','Doc.Publish','Doc.TransferOwner','Doc.Properties.Update','Doc.Users.Manage','Doc.Comments.Create','Doc.Comments.Update','Doc.Comments.Delete','Doc.Comments.Resolve'].includes(a);
}

Try / catch

try {
  await perms.assertDoc({ userId, workspaceId, docId, action });
} catch (e) {
  if (e instanceof DocActionDenied) {
    // downgrade UI to read-only mode for this doc
    return res.status(403).json({ docId, action: e.action });
  }
  throw e;
}

Prevention

When it happens

Trigger: Invoking assertDoc with an action the user's role does not grant (e.g. a 'Collaborator' trying Doc.Delete); doc-level overrides restricting a doc the workspace otherwise exposes; calling on a docId that exists but for which the user has no explicit grant; anonymous access to a non-published doc.

Common situations: Doc shared with reduced permissions then the user attempts to edit; permission rules changed mid-session; tests creating docs without granting Doc.Read to the acting user; client caching a doc after its access was downgraded.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/9658ca6b08ce0f06. Report an issue: GitHub.