immich-app/immich · error · BadRequestException

Cannot grant permissions you do not have

Error message

Cannot grant permissions you do not have

What it means

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.

Source

Thrown at server/src/services/api-key.service.ts:16

import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { ApiKey } from 'src/database';
import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { isGranted } from 'src/utils/access';

@Injectable()
export class ApiKeyService extends BaseService {
  async create(auth: AuthDto, dto: ApiKeyCreateDto): Promise<ApiKeyCreateResponseDto> {
    const token = this.cryptoRepository.randomBytesAsText(32);
    const hashed = this.cryptoRepository.hashSha256(token);

    if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
      throw new BadRequestException('Cannot grant permissions you do not have');
    }

    const entity = await this.apiKeyRepository.create({
      key: hashed,
      name: dto.name || 'API Key',
      userId: auth.user.id,
      permissions: dto.permissions,
    });

    return { secret: token, apiKey: this.map(entity) };
  }

  async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise<ApiKeyResponseDto> {
    const exists = await this.apiKeyRepository.getById(auth.user.id, id);
    if (!exists) {
      throw new BadRequestException('API Key not found');
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. 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
  2. Reduce the requested permissions to a subset of the authenticating key's permissions
  3. Create/assign the authenticating key with Permission.All so it can mint any scope
  4. Filter dto.permissions through the caller's own permissions list before submitting

Example fix

// before
await sdk.createApiKey({ name: 'worker', permissions: ['asset.read','asset.update','asset.delete'] }, { key: readOnlyKey });
// after — authenticate with a session, or intersect permissions
const requested = ['asset.read','asset.update','asset.delete'].filter(p => myKeyPermissions.includes(p));
await sdk.createApiKey({ name: 'worker', permissions: requested }, { key: readOnlyKey });
Defensive patterns

Strategy: validation

Validate before calling

// Before creating a key with API-key auth, intersect requested permissions
// with the authenticating key's permissions (from GET /api-keys/me).
const me = await sdk.getMyApiKey({ apiKey });
const current = me.permissions;
const hasAll = current.includes('all');
const safe = hasAll ? requested : requested.filter(p => current.includes(p));
if (safe.length !== requested.length) {
  throw new Error('Requested permissions exceed current key scope');
}
await sdk.createApiKey({ name, permissions: safe }, { apiKey });

Type guard

// Narrow an API-key auth context before relying on auth.apiKey.permissions
function hasApiKeyAuth(auth: { apiKey?: { permissions: string[] } | null }): auth is { apiKey: { permissions: string[] } } {
  return !!auth?.apiKey;
}

function canGrant(current: string[], requested: string[]): boolean {
  return current.includes('all') || requested.every(p => current.includes(p));
}

Prevention

When it happens

Trigger: 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].

Common situations: 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.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/c05872e653fbd49d. Report an issue: GitHub.