toeverything/AFFiNE · error · ActionForbidden

action_forbidden

action_forbidden

Error message

You are not allowed to perform this action.

What it means

Thrown by the `AdminGuard` canActivate when the request's session user is not a server administrator (`feature.isAdmin(req.session.user.id)` resolves false, or there is no session). The guard protects any resolver/method decorated with `@Admin()` and rejects with `action_forbidden` (category `action_forbidden`).

Source

Thrown at packages/backend/server/src/core/common/admin-guard.ts:30

@Injectable()
export class AdminGuard implements CanActivate, OnModuleInit {
  private feature!: FeatureService;

  constructor(private readonly ref: ModuleRef) {}

  onModuleInit() {
    this.feature = this.ref.get(FeatureService, { strict: false });
  }

  async canActivate(context: ExecutionContext) {
    const { req } = getRequestResponseFromContext(context);
    let allow = false;
    if (req.session) {
      allow = await this.feature.isAdmin(req.session.user.id);
    }

    if (!allow) {
      throw new ActionForbidden();
    }

    return true;
  }
}

/**
 * This guard is used to protect routes/queries/mutations that require a user to be administrator.
 *
 * @example
 *
 * ```typescript
 * \@Admin()
 * \@Mutation(() => UserType)
 * createAccount(userInput: UserInput) {
 *   // ...
 * }
 * ```

View on GitHub (pinned to 26c515e050)

Solutions

  1. Gate admin-only UI behind a client-side `isAdmin` flag fetched from the session/features so non-admins never trigger the call.
  2. On `action_forbidden`, redirect to a 403 page or sign the user out if the session is stale.
  3. For self-hosted setups, ensure the bootstrap/seed flow grants the initial user admin (check `FeatureService.isAdmin` wiring and admin flag config).
  4. Audit that `@Admin()` is only applied to genuinely admin-only operations.

Example fix

// before
await client.mutate({ mutation: SOME_ADMIN_MUTATION, variables });

// after
const { data } = await client.query({ query: CURRENT_USER_IS_ADMIN });
if (!data?.me?.isAdmin) {
  router.replace('/403');
  return;
}
await client.mutate({ mutation: SOME_ADMIN_MUTATION, variables });
Defensive patterns

Strategy: validation

Validate before calling

// Hide/disable admin actions unless the current user is an admin
const { data } = await client.query({ query: CURRENT_USER_IS_ADMIN_QUERY });
const isAdmin = Boolean(data?.me?.isAdmin);

if (!isAdmin) {
  router.replace('/403');
  return;
}

Type guard

function isAdminSession(value: unknown): value is { isAdmin: true } {
  return typeof value === 'object' &&
    value !== null &&
    (value as { isAdmin?: unknown }).isAdmin === true;
}

Try / catch

try {
  await mutateAdminAction(variables);
} catch (e) {
  const code = e?.graphQLErrors?.[0]?.extensions?.code;
  if (code === 'action_forbidden') {
    router.replace('/403'); // or sign out if session is stale
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling any `@Admin()`-guarded GraphQL mutation or route as a non-admin user; calling it with no session/an expired session; calling it before the feature service has populated admin flags.

Common situations: Front-end admin panels shown to users whose role changed; token expiry mid-session; self-hosted instances where the first user was not seeded as admin; mis-decorated resolvers accidentally requiring admin.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/e3c1067412edd8c9. Report an issue: GitHub.