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
- Gate admin-only UI behind a client-side `isAdmin` flag fetched from the session/features so non-admins never trigger the call.
- On `action_forbidden`, redirect to a 403 page or sign the user out if the session is stale.
- For self-hosted setups, ensure the bootstrap/seed flow grants the initial user admin (check `FeatureService.isAdmin` wiring and admin flag config).
- 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
- Gate all admin UI behind a client-side `isAdmin` flag from the session/features.
- On `action_forbidden`, treat the session as no-longer-admin and refresh credentials.
- For self-hosted setups, confirm the seed/bootstrap user is granted admin.
- Audit `@Admin()` usage so only truly admin-only operations are guarded.
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.