{"record":{"id":"747ce8abbc90a671","repo":"toeverything/AFFiNE","slug":"access-denied-747ce8","errorCode":"access_denied","errorMessage":"You do not have permission to access this resource.","messagePattern":"You do not have permission to access this resource\\.","errorType":"exception","errorClass":"AccessDenied","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/retrieval/artifact.ts","lineNumber":35,"sourceCode":"  private async authorize(userId: string, workspaceId: string) {\n    return await this.access\n      .user(userId)\n      .workspace(workspaceId)\n      .allowLocal()\n      .can('Workspace.Read');\n  }\n\n  async search(options: {\n    userId: string;\n    workspaceId: string;\n    query: string;\n    retrieval: RuntimeRetrievalScope;\n    limit: number;\n    messageId?: string;\n    signal?: AbortSignal;\n  }) {\n    if (!(await this.authorize(options.userId, options.workspaceId))) {\n      throw new AccessDenied();\n    }\n    let degraded = false;\n    let matched: Awaited<ReturnType<NativeEmbeddingService['match']>> = [];\n    try {\n      matched = await this.embedding.match(\n        options.workspaceId,\n        options.query,\n        'artifact',\n        options.retrieval,\n        options.limit,\n        options.signal\n      );\n    } catch (error) {\n      if (options.signal?.aborted) throw error;\n      degraded = true;\n    }\n    const matchedIds = new Set(matched.map(hit => hit.artifactId));\n    const missingRequired =","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/retrieval/artifact.ts#L17-L53","documentation":"The artifact retrieval service authorizes the user against the workspace before running embedding match; if authorize(userId, workspaceId) fails it throws AccessDenied (code `access_denied`, status `access_denied`). This gate protects workspace-scoped artifact (embedding) search from users who are not members of the workspace.","triggerScenarios":"Calling retrieval search with a userId that is not a member of workspaceId or lacks the required role; passing the wrong workspaceId (e.g. from another workspace's context); user removed from the workspace while their session/token is still valid.","commonSituations":"Cross-workspace id mixups in multi-workspace clients; permission revoked mid-session; self-hosted setups where workspace membership sync (e.g. from an identity provider) lags; tests using users never added to the workspace.","solutions":["Verify the user is an active member of the workspace with permission to use Copilot/retrieval","Double-check the workspaceId passed to the retrieval call matches the workspace the artifacts belong to","Refresh auth/session if membership was recently changed, then retry","For self-hosted: inspect workspace permissions in the DB to confirm the user-workspace relation exists"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// guard: confirm workspace access before retrieval search\nconst access = await checkWorkspacePermission(userId, workspaceId); // your ACL/API check\nif (!access.canRead) {\n  throw new Error('user has no access to workspace — skip artifact search');\n}\nawait artifactSearch({ userId, workspaceId, query, retrieval, limit });","typeGuard":"function isAccessDenied(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } })?.extensions?.code === 'access_denied';\n}","tryCatchPattern":"try {\n  await artifactSearch(params);\n} catch (e) {\n  if (isAccessDenied(e)) {\n    disableRetrievalForWorkspace(params.workspaceId); // stop retrying with same identity\n    return [];\n  }\n  throw e;\n}","preventionTips":["Derive workspaceId from the same context that authenticated the user, never from user input alone","Re-check membership when switching workspaces or after permission changes","Fail retrieval soft (empty results) so a permission gap degrades, not breaks, the chat flow"],"tags":["copilot","retrieval","artifact","permission","access-denied"],"backgroundTag":"access-denied-403","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}