{"record":{"id":"cf93cb4f535a216a","repo":"toeverything/AFFiNE","slug":"action-forbidden-cf93cb","errorCode":"action_forbidden","errorMessage":"BYOK settings require workspace owner or admin.","messagePattern":"BYOK settings require workspace owner or admin\\.","errorType":"exception","errorClass":"ActionForbidden","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/byok/policy.ts","lineNumber":34,"sourceCode":"    const state = await this.quotaState.reconcileUserQuotaState(userId);\n    const flags = state.flags as { unlimitedCopilot?: boolean };\n    return (\n      flags.unlimitedCopilot ||\n      ['pro', 'lifetime_pro', 'ai'].includes(state.plan)\n    );\n  }\n\n  async hasManagementAccess(workspaceId: string, userId?: string) {\n    if (!userId) return false;\n    const role = await this.models.workspaceUser.getActive(workspaceId, userId);\n    return (\n      role?.type === WorkspaceRole.Owner || role?.type === WorkspaceRole.Admin\n    );\n  }\n\n  async assertManagementAccess(workspaceId: string, userId?: string) {\n    if (!(await this.hasManagementAccess(workspaceId, userId))) {\n      throw new ActionForbidden(\n        'BYOK settings require workspace owner or admin.'\n      );\n    }\n  }\n\n  private async getWorkspaceOwnerId(workspaceId: string) {\n    const workspace = await this.models.workspace.get(workspaceId);\n    if (!workspace) {\n      return null;\n    }\n\n    try {\n      return (await this.models.workspaceUser.getOwner(workspaceId)).id;\n    } catch (error) {\n      if (\n        error instanceof Error &&\n        error.message === 'Workspace owner not found'\n      ) {","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/byok/policy.ts#L16-L52","documentation":"Authorization guard in the BYOK (bring-your-own-key) policy: mutating BYOK settings requires the caller to hold the Owner or Admin role in the target workspace, verified via workspaceUser.getActive. Members/visitors/collaborators — or unauthenticated or removed/suspended users (no active role record) — get ActionForbidden.","triggerScenarios":"A workspace Member calls update/create/delete BYOK profile endpoints or the BYOK settings GQL mutations; userId is undefined (session missing) so hasManagementAccess returns false; the caller's workspace membership was deactivated (getActive misses) between page load and save; cross-workspace request where the user is admin of a different workspace.","commonSituations":"Frontend shows the BYOK settings tab to non-admins based on stale role data; API scripts using a personal token from a member account; users demoted from admin while the settings page was open.","solutions":["Have a workspace Owner or Admin perform BYOK settings changes, or promote the user first.","Re-fetch the caller's workspace role before rendering the BYOK settings UI and hide it for non-admins.","Check the request carries the right session/user for the workspace in the URL/mutation input.","If membership looks wrong, confirm the workspaceUser record is active (not deleted/suspended)."],"exampleFix":"null","handlingStrategy":"validation","validationCode":"const role = await client.getMyWorkspaceRole(workspaceId);\nif (role !== 'Owner' && role !== 'Admin') {\n  hideByokSettings(); // only Owner/Admin may manage BYOK\n}","typeGuard":"const canManageByok = (roleType?: string): boolean =>\n  roleType === 'Owner' || roleType === 'Admin';","tryCatchPattern":"try {\n  await updateByokProfile(input);\n} catch (e) {\n  if (e?.code === 'action_forbidden' && /owner or admin/i.test(e.message)) {\n    showToast('Ask a workspace owner or admin to change BYOK settings');\n  } else throw e;\n}","preventionTips":["Re-fetch the active workspace role before rendering BYOK settings, not just at app load.","Keep mutations scoped to the workspace the session user actually administers.","Disable the save button for non-admins instead of letting the request fail."],"tags":["byok","copilot","authorization","workspace-roles","rbac"],"backgroundTag":"insufficient-permissions","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}