{"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":171,"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":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/permission/service.ts#L153-L189","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the doc access matrix with docPermissions() before issuing the mutating call.","Ensure the user's workspace role grants the underlying action and no doc-level override revokes it.","If using a custom permission policy, confirm the rule set includes the requested action for the role.","For anonymous/public flows, verify the doc is actually published before asserting Doc.Read."],"exampleFix":"// before\nawait perms.assertDoc({ userId, workspaceId, docId, action: 'Doc.Update' });\n\n// after\nconst decisions = await perms.docPermissions({ userId, workspaceId, docId, actions: ['Doc.Update'] });\nif (!decisions.decisions[0]?.allowed) {\n  throw new DocActionDenied({ action: 'Doc.Update', docId, spaceId: workspaceId });\n}","handlingStrategy":"validation","validationCode":"const { decisions } = await perms.docPermissions({ userId, workspaceId, docId, actions: [action] });\nif (!decisions[0]?.allowed) {\n  return { denied: true, docId, action };\n}","typeGuard":"function isDocAction(a: string): a is PermissionDocAction {\n  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);\n}","tryCatchPattern":"try {\n  await perms.assertDoc({ userId, workspaceId, docId, action });\n} catch (e) {\n  if (e instanceof DocActionDenied) {\n    // downgrade UI to read-only mode for this doc\n    return res.status(403).json({ docId, action: e.action });\n  }\n  throw e;\n}","preventionTips":["Call docPermissions once per doc batch and gate each subsequent action on its decision.","Refresh doc permissions when the user accepts a doc invite or a doc is re-shared.","In the client, disable edit controls when Doc.Update is denied rather than letting the request fail."],"tags":["permissions","authorization","doc","nestjs"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}