{"record":{"id":"c05872e653fbd49d","repo":"immich-app/immich","slug":"cannot-grant-permissions-you-do-not-have","errorCode":null,"errorMessage":"Cannot grant permissions you do not have","messagePattern":"Cannot grant permissions you do not have","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/api-key.service.ts","lineNumber":16,"sourceCode":"import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';\nimport { ApiKey } from 'src/database';\nimport { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto';\nimport { AuthDto } from 'src/dtos/auth.dto';\nimport { Permission } from 'src/enum';\nimport { BaseService } from 'src/services/base.service';\nimport { isGranted } from 'src/utils/access';\n\n@Injectable()\nexport class ApiKeyService extends BaseService {\n  async create(auth: AuthDto, dto: ApiKeyCreateDto): Promise<ApiKeyCreateResponseDto> {\n    const token = this.cryptoRepository.randomBytesAsText(32);\n    const hashed = this.cryptoRepository.hashSha256(token);\n\n    if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {\n      throw new BadRequestException('Cannot grant permissions you do not have');\n    }\n\n    const entity = await this.apiKeyRepository.create({\n      key: hashed,\n      name: dto.name || 'API Key',\n      userId: auth.user.id,\n      permissions: dto.permissions,\n    });\n\n    return { secret: token, apiKey: this.map(entity) };\n  }\n\n  async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise<ApiKeyResponseDto> {\n    const exists = await this.apiKeyRepository.getById(auth.user.id, id);\n    if (!exists) {\n      throw new BadRequestException('API Key not found');\n    }\n","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/api-key.service.ts#L1-L34","documentation":"Thrown by ApiKeyService.create when the request is authenticated with an API key (auth.apiKey is set) and the new key requests permissions that are not a subset of the authenticating key's permissions. The isGranted helper returns true only if `current` contains Permission.All OR every requested permission is in `current`. This prevents permission escalation: a limited-scope API key cannot mint a broader-scope key.","triggerScenarios":"POST /api-keys while authenticated via an API key whose permissions array does not cover every permission listed in the request body's `permissions` field. For example, a key with only [AssetRead] trying to create a key with [AssetRead, AssetUpdate, AssetDelete].","commonSituations":"Scripts or admin tooling that authenticate with a scoped API key then attempt to provision full-permission keys; CI that rotates keys using a least-privilege key; misunderstanding that API-key-created keys cannot exceed the creator's scope.","solutions":["Authenticate the create call with a web session (cookie/JWT) instead of an API key — session auth has no auth.apiKey and the check is skipped entirely","Reduce the requested permissions to a subset of the authenticating key's permissions","Create/assign the authenticating key with Permission.All so it can mint any scope","Filter dto.permissions through the caller's own permissions list before submitting"],"exampleFix":"// before\nawait sdk.createApiKey({ name: 'worker', permissions: ['asset.read','asset.update','asset.delete'] }, { key: readOnlyKey });\n// after — authenticate with a session, or intersect permissions\nconst requested = ['asset.read','asset.update','asset.delete'].filter(p => myKeyPermissions.includes(p));\nawait sdk.createApiKey({ name: 'worker', permissions: requested }, { key: readOnlyKey });","handlingStrategy":"validation","validationCode":"// Before creating a key with API-key auth, intersect requested permissions\n// with the authenticating key's permissions (from GET /api-keys/me).\nconst me = await sdk.getMyApiKey({ apiKey });\nconst current = me.permissions;\nconst hasAll = current.includes('all');\nconst safe = hasAll ? requested : requested.filter(p => current.includes(p));\nif (safe.length !== requested.length) {\n  throw new Error('Requested permissions exceed current key scope');\n}\nawait sdk.createApiKey({ name, permissions: safe }, { apiKey });","typeGuard":"// Narrow an API-key auth context before relying on auth.apiKey.permissions\nfunction hasApiKeyAuth(auth: { apiKey?: { permissions: string[] } | null }): auth is { apiKey: { permissions: string[] } } {\n  return !!auth?.apiKey;\n}\n\nfunction canGrant(current: string[], requested: string[]): boolean {\n  return current.includes('all') || requested.every(p => current.includes(p));\n}","tryCatchPattern":null,"preventionTips":["Use web-session auth for administrative key creation and permission edits","Store and intersect the caller's own permission list before submitting create/update requests","Treat Permission.All as the only grant-all scope; any other key is bounded"],"tags":["api-key","permissions","authorization","permission-escalation","nestjs"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}