{"record":{"id":"9658ca6b08ce0f06","repo":"toeverything/AFFiNE","slug":"doc-action-denied","errorCode":"doc_action_denied","errorMessage":"You do not have permission to perform ${action} action on doc ${docId}.","messagePattern":"You do not have permission to perform (.+?) action on doc (.+?)\\.","errorType":"exception","errorClass":"DocActionDenied","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/permission/service.ts","lineNumber":156,"sourceCode":"    action: PermissionDocAction;\n    allowLocal?: boolean;\n  }) {\n    const output = await this.docPermissions({\n      ...input,\n      actions: [input.action],\n    });\n    return output.decisions[0]?.allowed ?? false;\n  }\n\n  async assertDoc(input: {\n    userId?: string;\n    workspaceId: string;\n    docId: string;\n    action: PermissionDocAction;\n    allowLocal?: boolean;\n  }) {\n    if (!(await this.canDoc(input))) {\n      throw new DocActionDenied({\n        action: input.action,\n        docId: input.docId,\n        spaceId: input.workspaceId,\n      });\n    }\n  }\n\n  async filterReadableDocs<T extends { docId: string }>(input: {\n    userId?: string;\n    workspaceId: string;\n    docs: T[];\n    allowLocal?: boolean;\n  }) {\n    const decisions = await this.batchDocPermissions({\n      ...input,\n      docs: input.docs.map(doc => ({\n        docId: doc.docId,\n        actions: ['Doc.Read'],","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/591f874dad30887a80143a061a44bd3ca7ee3299/packages/backend/server/src/core/permission/service.ts#L138-L174","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the user has an active workspace membership (workspaceUser.getActive) for the doc's workspace.","Pre-check with the non-throwing PermissionService.canDoc({ userId, workspaceId, docId, action }) before performing the call.","Filter doc lists through filterReadableDocs instead of asserting per doc.","For local workspaces pass allowLocal: true where the call supports it."],"exampleFix":"// before\nawait docService.update({ userId, workspaceId, docId, data }); // may throw doc_action_denied\n\n// after\nconst allowed = await permission.canDoc({ userId, workspaceId, docId, action: 'Doc.Update' });\nif (!allowed) throw new Forbidden('no access');\nawait docService.update({ userId, workspaceId, docId, data });","handlingStrategy":"validation","validationCode":"const allowed = await permission.canDoc({\n  userId,\n  workspaceId,\n  docId,\n  action: 'Doc.Update',\n});\nif (!allowed) {\n  return res.status(403).json({ code: 'doc_action_denied' });\n}","typeGuard":"const isDocActionDenied = (e: unknown): e is DocActionDenied =>\n  e instanceof DocActionDenied;","tryCatchPattern":"try {\n  await permission.assertDoc({ userId, workspaceId, docId, action });\n} catch (e) {\n  if (e instanceof DocActionDenied) return res.status(403).json({ code: e.code });\n  throw e;\n}","preventionTips":["Resolve permissions with canDoc before doc mutations instead of relying on assertDoc to fail.","Pre-filter doc collections with filterReadableDocs so bulk operations never hit per-doc denials.","Drop sessions and realtime rooms immediately when workspace membership is revoked."],"tags":["permissions","authorization","docs","workspace"],"backgroundTag":"permission-denied","analyzedSha":"591f874dad30887a80143a061a44bd3ca7ee3299","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}