immich-app/immich · error · BadRequestException
Cannot rotate an API Key with permissions you do not have
Error message
Cannot rotate an API Key with permissions you do not have
What it means
Immich's ApiKeyService.rotate (POST /api-keys/:id/rotate) refuses to rotate an API key when the request itself is authenticated with an API key whose permissions do not fully cover the key being rotated. It uses isGranted (server/src/utils/access.ts:13), which checks that the target key's permissions are a subset of the calling credentials (Permission.All acts as a wildcard). This prevents a narrowly-scoped key from regenerating — and thereby learning the plaintext secret of — a more privileged key. It surfaces as a NestJS BadRequestException (HTTP 400).
Source
Thrown at server/src/services/api-key.service.ts:57
dto.permissions &&
!isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })
) {
throw new BadRequestException('Cannot grant permissions you do not have');
}
const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions });
return this.map(key);
}
async rotate(auth: AuthDto, id: string): Promise<ApiKeyCreateResponseDto> {
const existing = await findOrFail(() => this.apiKeyRepository.getById(auth.user.id, id), 'API Key not found');
if (
auth.apiKey &&
!isGranted({ requested: existing.permissions as Permission[], current: auth.apiKey.permissions })
) {
throw new BadRequestException('Cannot rotate an API Key with permissions you do not have');
}
const token = this.cryptoRepository.randomBytesAsText(32);
const hashed = this.cryptoRepository.hashSha256(token);
const newKey = await this.apiKeyRepository.update(auth.user.id, id, { key: hashed });
const apiKey = this.map(newKey);
return { ...apiKey, secret: token, apiKey };
}
async delete(auth: AuthDto, id: string): Promise<void> {
const exists = await this.apiKeyRepository.getById(auth.user.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
}
await this.apiKeyRepository.delete(auth.user.id, id);
}View on GitHub (pinned to 37e033a09d)
Solutions
- Authenticate the rotate call with the user's web session (cookie / OAuth access token) instead of an API key — the permission check only applies when the caller is an API key.
- Or use a calling key whose permission set is a superset of the target key's — simplest is a key created with all permissions (Permission.All).
- Or first update the calling key's permissions (or delete and recreate it) so it includes every permission the target key holds, then retry the rotation.
- In scripts, pre-flight the check: fetch the target key, compare permission arrays, and skip or escalate before calling rotate.
Example fix
// before — scoped key trying to rotate a broader key
await fetch(`${IMMICH_URL}/api-keys/${id}/rotate`, {
method: 'POST',
headers: { 'x-api-key': SCOPED_READ_ONLY_KEY },
});
// => 400 { message: 'Cannot rotate an API Key with permissions you do not have' }
// after — rotate with the user's session or an all-permission key
await fetch(`${IMMICH_URL}/api-keys/${id}/rotate`, {
method: 'POST',
credentials: 'include', // session cookie; or x-api-key from a key granted all permissions
}); Defensive patterns
Strategy: validation
Validate before calling
// Before rotating: fetch the target key and compare permission sets client-side.
const target = await api.getApiKey(id); // GET /api-keys/{id}
const callingPermissions = parseCallingKeyPermissions(); // permissions of the key making this request
const canRotate =
callingPermissions.includes('all') ||
target.permissions.every((p) => callingPermissions.includes(p));
if (!canRotate) {
throw new Error('Rotate from a session or from a key granted every permission the target key has');
}
await api.rotateApiKey(id); // POST /api-keys/{id}/rotate Type guard
const isPermissionArray = (value: unknown): value is Permission[] => Array.isArray(value) && value.every((p) => typeof p === 'string');
Try / catch
try {
return await api.rotateApiKey(id);
} catch (error) {
if (isHttpError(error, 400, 'Cannot rotate an API Key with permissions you do not have')) {
// credential mismatch, not a transient failure: re-authenticate (session / all-permission key) instead of retrying
throw new RotationTokenEscalationError('Re-run rotation with a session cookie or an unrestricted key');
}
throw error;
} Prevention
- Rotate keys from the web UI session or from a key created with all permissions; never from a narrowly scoped automation key.
- Keep automation keys' permission scope at least as broad as every key they are allowed to manage.
- Treat a 400 on rotate as a permissions-design signal, not a retryable error.
When it happens
Trigger: Calling POST /api-keys/{id}/rotate with an x-api-key header whose key (a) is not Permission.All and (b) lacks at least one permission that the target key has. Example: a key with only asset.read tries to rotate the admin's full-permission key. Requests authenticated with the user's session (cookie/OAuth) never hit this check because auth.apiKey is undefined in that case.
Common situations: Automation/CI scripts that use a scoped API key but try to rotate every key in the account, including the owner's unrestricted one. Rotating a key using an older key that was created with a narrower permission set before permissions were introduced. Key-management runbooks that assume API keys can manage all keys in the account.
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
- Cannot grant permissions you do not have
- Missing required permission: ${requestedPermission}
- Not authenticated with an API Key
- Invalid import path: ${path.message}
- Sidecar files cannot be deleted
AI-assisted analysis of immich-app/immich@37e033a09d (2026-08-21).
Data as JSON: /api/errors/29550e8d5ef2b2c8.
Report an issue: GitHub.