{"record":{"id":"686479db8dec8735","repo":"toeverything/AFFiNE","slug":"expect-to-grant-doc-user-roles","errorCode":"expect_to_grant_doc_user_roles","errorMessage":"Expect doc not to be workspace","messagePattern":"Expect doc not to be workspace","errorType":"exception","errorClass":"ExpectToGrantDocUserRoles","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/doc.ts","lineNumber":748,"sourceCode":"    );\n  }\n\n  @Mutation(() => Boolean)\n  async grantDocUserRoles(\n    @CurrentUser() user: CurrentUser,\n    @Args('input') input: GrantDocUserRolesInput\n  ): Promise<boolean> {\n    const pairs = {\n      spaceId: input.workspaceId,\n      docId: input.docId,\n    };\n\n    if (input.workspaceId === input.docId) {\n      this.logger.error(\n        'Expect to grant doc user roles, but it is a workspace',\n        pairs\n      );\n      throw new ExpectToGrantDocUserRoles(\n        pairs,\n        'Expect doc not to be workspace'\n      );\n    }\n\n    const role = toDomainDocRole(input.role);\n    if (!role || role === 'owner') {\n      throw new ExpectToGrantDocUserRoles(pairs, 'Invalid grant role');\n    }\n    try {\n      await this.runtime.executeDomainCommandV1({\n        command: 'grant_doc_roles',\n        actorUserId: user.id,\n        workspaceId: input.workspaceId,\n        docId: input.docId,\n        targetUserIds: input.userIds,\n        newRole: role,\n      });","sourceCodeStart":730,"sourceCodeEnd":766,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L730-L766","documentation":"grantDocUserRoles rejects input where workspaceId === docId: the workspace root is a Space, and doc-level user roles only apply to real pages. The check runs before Doc.Users.Manage is asserted, so this fires even for callers who would otherwise be authorized.","triggerScenarios":"Calling grantDocUserRoles with input.docId equal to input.workspaceId — client building the input from a workspace/root object or defaulting docId to the workspace id.","commonSituations":"Sharing flows that treat the workspace root as a page; forms where docId was never populated and fell back to the space id.","solutions":["Validate input client-side: workspaceId and docId must differ before calling the mutation","Source docId from a page/doc object, never from the workspace","For workspace-wide roles use the workspace member APIs instead of doc roles"],"exampleFix":"// before\nawait grantDocUserRoles({ workspaceId: ws.id, docId: ws.id, userIds, role }); // root -> EXPECT_TO_GRANT_DOC_USER_ROLES\n\n// after\nconst page = getSelectedPage(); // real page id\nif (!page || page.id === ws.id) throw new Error('select a page first');\nawait grantDocUserRoles({ workspaceId: ws.id, docId: page.id, userIds, role });","handlingStrategy":"validation","validationCode":"// Validate the grant input before calling the mutation\nfunction validDocRoleInput(workspaceId: string, docId: string | undefined): boolean {\n  return !!docId && docId !== workspaceId;\n}\nif (!validDocRoleInput(input.workspaceId, input.docId)) {\n  throw new Error('docId must reference a page, not the workspace');\n}\nawait grantDocUserRoles(input);","typeGuard":"function isExpectToGrantDocUserRoles(e: unknown): boolean {\n  return (\n    typeof e === 'object' && e !== null &&\n    (e as { extensions?: { code?: string } }).extensions?.code === 'expect_to_grant_doc_user_roles'\n  );\n}","tryCatchPattern":"try {\n  await grantDocUserRoles(input);\n} catch (e) {\n  if (isExpectToGrantDocUserRoles(e)) {\n    resetShareDialogToPageSelection(); // user targeted the root; ask for a page\n  } else throw e;\n}","preventionTips":["Hide doc-sharing actions on the workspace root node","Build mutation inputs from a selected page object, with docId required and non-defaulted","Reuse one shared id guard across grant/revoke/update doc-role call sites"],"tags":["doc","permissions","validation","affine"],"backgroundTag":"invalid-identifier","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"}