{"record":{"id":"29550e8d5ef2b2c8","repo":"immich-app/immich","slug":"cannot-rotate-an-api-key-with-permissions-you-do-n","errorCode":null,"errorMessage":"Cannot rotate an API Key with permissions you do not have","messagePattern":"Cannot rotate an API Key with permissions you do not have","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/api-key.service.ts","lineNumber":57,"sourceCode":"      dto.permissions &&\n      !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })\n    ) {\n      throw new BadRequestException('Cannot grant permissions you do not have');\n    }\n\n    const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions });\n\n    return this.map(key);\n  }\n\n  async rotate(auth: AuthDto, id: string): Promise<ApiKeyCreateResponseDto> {\n    const existing = await findOrFail(() => this.apiKeyRepository.getById(auth.user.id, id), 'API Key not found');\n\n    if (\n      auth.apiKey &&\n      !isGranted({ requested: existing.permissions as Permission[], current: auth.apiKey.permissions })\n    ) {\n      throw new BadRequestException('Cannot rotate an API Key with permissions you do not have');\n    }\n\n    const token = this.cryptoRepository.randomBytesAsText(32);\n    const hashed = this.cryptoRepository.hashSha256(token);\n    const newKey = await this.apiKeyRepository.update(auth.user.id, id, { key: hashed });\n    const apiKey = this.map(newKey);\n\n    return { ...apiKey, secret: token, apiKey };\n  }\n\n  async delete(auth: AuthDto, id: string): Promise<void> {\n    const exists = await this.apiKeyRepository.getById(auth.user.id, id);\n    if (!exists) {\n      throw new BadRequestException('API Key not found');\n    }\n\n    await this.apiKeyRepository.delete(auth.user.id, id);\n  }","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/immich-app/immich/blob/37e033a09d212fca3d273990f138112abb9e0837/server/src/services/api-key.service.ts#L39-L75","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — scoped key trying to rotate a broader key\nawait fetch(`${IMMICH_URL}/api-keys/${id}/rotate`, {\n  method: 'POST',\n  headers: { 'x-api-key': SCOPED_READ_ONLY_KEY },\n});\n// => 400 { message: 'Cannot rotate an API Key with permissions you do not have' }\n\n// after — rotate with the user's session or an all-permission key\nawait fetch(`${IMMICH_URL}/api-keys/${id}/rotate`, {\n  method: 'POST',\n  credentials: 'include', // session cookie; or x-api-key from a key granted all permissions\n});","handlingStrategy":"validation","validationCode":"// Before rotating: fetch the target key and compare permission sets client-side.\nconst target = await api.getApiKey(id); // GET /api-keys/{id}\nconst callingPermissions = parseCallingKeyPermissions(); // permissions of the key making this request\n\nconst canRotate =\n  callingPermissions.includes('all') ||\n  target.permissions.every((p) => callingPermissions.includes(p));\n\nif (!canRotate) {\n  throw new Error('Rotate from a session or from a key granted every permission the target key has');\n}\nawait api.rotateApiKey(id); // POST /api-keys/{id}/rotate","typeGuard":"const isPermissionArray = (value: unknown): value is Permission[] =>\n  Array.isArray(value) && value.every((p) => typeof p === 'string');","tryCatchPattern":"try {\n  return await api.rotateApiKey(id);\n} catch (error) {\n  if (isHttpError(error, 400, 'Cannot rotate an API Key with permissions you do not have')) {\n    // credential mismatch, not a transient failure: re-authenticate (session / all-permission key) instead of retrying\n    throw new RotationTokenEscalationError('Re-run rotation with a session cookie or an unrestricted key');\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["immich","api-key","permissions","authorization","http-400","key-rotation"],"backgroundTag":"insufficient-permissions","analyzedSha":"37e033a09d212fca3d273990f138112abb9e0837","analyzedAt":"2026-08-21T18:08:19.313Z","contentChangedAt":"2026-08-21T18:08:19.313Z","schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}