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
- 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
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
- 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
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
- Not authenticated with an API Key
- Missing required permission: ${requestedPermission}
- API Key not found
- Forbidden
- Invalid import path: ${path.message}
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/c05872e653fbd49d.
Report an issue: GitHub.