{"record":{"id":"c8d86219b835eab3","repo":"lobehub/lobehub","slug":"forbidden-c8d862","errorCode":"FORBIDDEN","errorMessage":"Topic comment resource not found","messagePattern":"Topic comment resource not found","errorType":"error_code","errorClass":"TRPCError","httpStatus":403,"severity":"error","filePath":"apps/server/src/routers/lambda/_helpers/topicCommentAccess.ts","lineNumber":26,"sourceCode":"import { assertCanViewTopicTargets } from './conversationResourceGuard';\n\nexport const assertTopicCommentReadAccess = async (params: {\n  db: LobeChatDatabase;\n  grantedPermissions?: readonly string[];\n  hideExistence?: boolean;\n  topicId: string;\n  userId: string;\n  workspaceId: string;\n}) => {\n  const permissions = await getWorkspaceScopedPermissionMatches({\n    action: 'TOPIC_COMMENT_READ',\n    db: params.db,\n    grantedPermissions: params.grantedPermissions,\n    userId: params.userId,\n    workspaceId: params.workspaceId,\n  });\n  if (!permissions.hasAllScope && !permissions.hasOwnerScope) {\n    throw new TRPCError({\n      code: params.hideExistence ? 'NOT_FOUND' : 'FORBIDDEN',\n      message: 'Topic comment resource not found',\n    });\n  }\n\n  const [topic] = await params.db\n    .select({ id: topics.id })\n    .from(topics)\n    .where(and(eq(topics.id, params.topicId), eq(topics.workspaceId, params.workspaceId)))\n    .limit(1);\n  if (!topic) {\n    throw new TRPCError({ code: 'NOT_FOUND', message: 'Topic comment resource not found' });\n  }\n\n  try {\n    await assertCanViewTopicTargets(\n      {\n        db: params.db,","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/lobehub/lobehub/blob/10f24d7ade75139093a9373b364f6bc91f3cd7db/apps/server/src/routers/lambda/_helpers/topicCommentAccess.ts#L8-L44","documentation":"Permission gate at the entry of assertTopicCommentReadAccess. The caller lacks TOPIC_COMMENT_READ with either 'all' or 'owner' scope in this workspace, so the read is denied before the topic is even queried. hideExistence defaults to FORBIDDEN, but callers that want to hide resource existence (e.g. unauthenticated shared-link readers) pass hideExistence: true to downgrade to NOT_FOUND.","triggerScenarios":"Calling a topic-comment read procedure (list/get comments on a topic) when the workspace caller's grantedPermissions do not include a TOPIC_COMMENT_READ grant with workspace-wide or owner scope. Triggered by viewers/members without the comment-read permission, or by callers from a different workspace.","commonSituations":"A workspace viewer (read-only role) trying to read comments when only members have the grant; permission grants not yet provisioned after a workspace role change; a stale permission cache after a role downgrade.","solutions":["Grant the user TOPIC_COMMENT_READ (all or owner scope) for the workspace via the workspace permission admin.","Confirm the caller's role — viewers may need promotion to member.","If the caller is the topic creator and should inherently have read access, verify the permission resolution path includes creator-scope fallback."],"exampleFix":"// before: caller has no TOPIC_COMMENT_READ grant\nawait trpc.topicComment.list.query({ topicId, workspaceId }); // FORBIDDEN\n\n// after: admin grants the permission\nawait admin.grantWorkspacePermission(userId, workspaceId, 'TOPIC_COMMENT_READ', 'all');","handlingStrategy":"validation","validationCode":"const canReadComments = (grantedPermissions: readonly string[] | undefined) =>\n  grantedPermissions?.some((p) => p.includes('TOPIC_COMMENT_READ')) ?? false;\nif (!canReadComments(session.grantedPermissions)) {\n  // hide the comment UI instead of letting the call fail\n}","typeGuard":"null","tryCatchPattern":"try {\n  await trpc.topicComment.list.query({ topicId, workspaceId });\n} catch (e) {\n  if (e.code === 'FORBIDDEN') hideCommentUI();\n}","preventionTips":["Drive the comment UI's visibility from the resolved permission set, not from role name.","Re-resolve permissions after a role change.","Cache grantedPermissions per session and refresh on workspace switch."],"tags":["authorization","workspace","rbac","topic-comment","forbidden","permissions"],"backgroundTag":null,"analyzedSha":"10f24d7ade75139093a9373b364f6bc91f3cd7db","analyzedAt":"2026-08-12T11:43:19.543Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}