toeverything/AFFiNE · error · ActionForbidden

action_forbidden

action_forbidden

Error message

BYOK settings require workspace owner or admin.

What it means

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.

Source

Thrown at packages/backend/server/src/plugins/copilot/byok/policy.ts:34

    const state = await this.quotaState.reconcileUserQuotaState(userId);
    const flags = state.flags as { unlimitedCopilot?: boolean };
    return (
      flags.unlimitedCopilot ||
      ['pro', 'lifetime_pro', 'ai'].includes(state.plan)
    );
  }

  async hasManagementAccess(workspaceId: string, userId?: string) {
    if (!userId) return false;
    const role = await this.models.workspaceUser.getActive(workspaceId, userId);
    return (
      role?.type === WorkspaceRole.Owner || role?.type === WorkspaceRole.Admin
    );
  }

  async assertManagementAccess(workspaceId: string, userId?: string) {
    if (!(await this.hasManagementAccess(workspaceId, userId))) {
      throw new ActionForbidden(
        'BYOK settings require workspace owner or admin.'
      );
    }
  }

  private async getWorkspaceOwnerId(workspaceId: string) {
    const workspace = await this.models.workspace.get(workspaceId);
    if (!workspace) {
      return null;
    }

    try {
      return (await this.models.workspaceUser.getOwner(workspaceId)).id;
    } catch (error) {
      if (
        error instanceof Error &&
        error.message === 'Workspace owner not found'
      ) {

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Have a workspace Owner or Admin perform BYOK settings changes, or promote the user first.
  2. Re-fetch the caller's workspace role before rendering the BYOK settings UI and hide it for non-admins.
  3. Check the request carries the right session/user for the workspace in the URL/mutation input.
  4. If membership looks wrong, confirm the workspaceUser record is active (not deleted/suspended).

Example fix

null
Defensive patterns

Strategy: validation

Validate before calling

const role = await client.getMyWorkspaceRole(workspaceId);
if (role !== 'Owner' && role !== 'Admin') {
  hideByokSettings(); // only Owner/Admin may manage BYOK
}

Type guard

const canManageByok = (roleType?: string): boolean =>
  roleType === 'Owner' || roleType === 'Admin';

Try / catch

try {
  await updateByokProfile(input);
} catch (e) {
  if (e?.code === 'action_forbidden' && /owner or admin/i.test(e.message)) {
    showToast('Ask a workspace owner or admin to change BYOK settings');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/cf93cb4f535a216a. Report an issue: GitHub.