{"record":{"id":"907b1016104fa276","repo":"toeverything/AFFiNE","slug":"doc-action-denied-907b10","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/workspaces/controller.ts","lineNumber":67,"sourceCode":"\n  private buildVisitorId(req: Request, workspaceId: string, docId: string) {\n    const tracker = getRequestTrackerId(req);\n    return createHash('sha256')\n      .update(`${workspaceId}:${docId}:${tracker}`)\n      .digest('hex');\n  }\n\n  private async assertCanReadPublicDoc(\n    userId: string,\n    workspaceId: string,\n    docId: string\n  ) {\n    const canReadSharedDoc = await this.ac\n      .user(userId)\n      .doc(workspaceId, docId)\n      .can('Doc.Read');\n    if (!canReadSharedDoc) {\n      throw new DocActionDenied({\n        docId,\n        spaceId: workspaceId,\n        action: 'Doc.Read',\n      });\n    }\n  }\n\n  private async getPublishModeHeader(workspaceId: string, docId: string) {\n    const docMeta = await this.models.doc.getMeta(workspaceId, docId, {\n      select: {\n        mode: true,\n      },\n    });\n    return docMeta?.mode === PublicDocMode.Edgeless\n      ? DocMode.edgeless\n      : DocMode.page;\n  }\n","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/controller.ts#L49-L85","documentation":"Thrown by WorkspacesController.assertCanReadPublicDoc when the acting user fails the Doc.Read capability check on (workspaceId, docId) — this.ac.user(userId).doc(...).can('Doc.Read') returned false. It guards shared/public-doc endpoints that still require at least read access, and carries docId, spaceId, and the failed action ('Doc.Read') in the error payload.","triggerScenarios":"Hitting endpoints that funnel through assertCanReadPublicDoc (e.g. public doc reads and the comment-attachment route) for a doc that is not shared with the caller and where the caller has no workspace member role granting Doc.Read.","commonSituations":"Opening a link to a doc whose public sharing was revoked or never enabled; logged-out visitors hitting member-only docs; wrong workspaceId/docId pair (doc exists in another space); membership or doc-access-policy rows not yet visible after a role change.","solutions":["Share the doc with the user or publish it publicly (workspace/doc sharing settings) so Doc.Read evaluates true","Verify the URL pairs the correct workspaceId and docId — a mismatched pair fails the check","Authenticate as a workspace member (owner/admin/member) before requesting the doc","Server-side, confirm the docAccessPolicy/sharing rows exist for that doc — they are what let outsiders pass Doc.Read"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// before requesting a shared doc, confirm read access if your client has a capability probe\nconst canRead = await graphql(`\n  query CanRead($wsId: String!, $docId: String!) {\n    workspace(id: $wsId) { doc(id: $docId) { canRead } }\n  }\n`, { wsId, docId });\nif (!canRead) {\n  showAccessRequest(wsId, docId);\n  return;\n}","typeGuard":"function isDocActionDenied(e: unknown): e is { code: 'doc_action_denied'; docId: string; spaceId: string; action: string } {\n  return typeof e === 'object' && e !== null && (e as any).code === 'doc_action_denied';\n}","tryCatchPattern":"try {\n  return await fetchPublicDoc(wsId, docId);\n} catch (e) {\n  if (isDocActionDenied(e)) {\n    return renderNoAccessPage(e.docId); // 403: do not retry with same identity\n  }\n  throw e;\n}","preventionTips":["Before deep-linking, verify the doc is shared with the current user (or public)","Keep auth tokens fresh so requests don't degrade to anonymous and lose Doc.Read","On permission revocation webhooks/events, purge cached doc links from the UI","Always pair workspaceId and docId from the same source (doc list), never mix"],"tags":["authorization","permission","doc","sharing","access-control"],"backgroundTag":"permission-denied","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}