From 81c93101a0e2849d846eb812dd8e4fa6570bc2f4 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 12 Feb 2026 12:08:20 -0500 Subject: [PATCH] feat: verify permissions (#25647) --- cli/src/commands/asset.ts | 6 +++--- cli/src/commands/auth.ts | 13 +++++++++++-- cli/src/commands/server-info.ts | 5 +++-- cli/src/utils.ts | 32 +++++++++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index ff7b609eef..65773049df 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -4,6 +4,7 @@ import { AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + Permission, addAssetsToAlbum, checkBulkUpload, createAlbum, @@ -20,13 +21,11 @@ import { Stats, createReadStream } from 'node:fs'; import { stat, unlink } from 'node:fs/promises'; import path, { basename } from 'node:path'; import { Queue } from 'src/queue'; -import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils'; +import { BaseOptions, Batcher, authenticate, crawl, requirePermissions, s, sha1 } from 'src/utils'; const UPLOAD_WATCH_BATCH_SIZE = 100; const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000; -const s = (count: number) => (count === 1 ? '' : 's'); - // TODO figure out why `id` is missing type AssetBulkUploadCheckResults = Array; type Asset = { id: string; filepath: string }; @@ -136,6 +135,7 @@ export const startWatch = async ( export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { await authenticate(baseOptions); + await requirePermissions([Permission.AssetUpload]); const scanFiles = await scan(paths, options); diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index f0011c6a24..1e1efa97b4 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -1,7 +1,15 @@ -import { getMyUser } from '@immich/sdk'; +import { getMyUser, Permission } from '@immich/sdk'; import { existsSync } from 'node:fs'; import { mkdir, unlink } from 'node:fs/promises'; -import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; +import { + BaseOptions, + connect, + getAuthFilePath, + logError, + requirePermissions, + withError, + writeAuthFile, +} from 'src/utils'; export const login = async (url: string, key: string, options: BaseOptions) => { console.log(`Logging in to ${url}`); @@ -9,6 +17,7 @@ export const login = async (url: string, key: string, options: BaseOptions) => { const { configDirectory: configDir } = options; await connect(url, key); + await requirePermissions([Permission.UserRead]); const [error, user] = await withError(getMyUser()); if (error) { diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts index bea49231c9..9a5098e628 100644 --- a/cli/src/commands/server-info.ts +++ b/cli/src/commands/server-info.ts @@ -1,8 +1,9 @@ -import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; -import { BaseOptions, authenticate } from 'src/utils'; +import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes, Permission } from '@immich/sdk'; +import { authenticate, BaseOptions, requirePermissions } from 'src/utils'; export const serverInfo = async (options: BaseOptions) => { const { url } = await authenticate(options); + await requirePermissions([Permission.ServerAbout, Permission.AssetStatistics, Permission.UserRead]); const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([ getServerVersion(), diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 9ef20b3679..38bd119459 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,4 +1,4 @@ -import { getMyUser, init, isHttpError } from '@immich/sdk'; +import { ApiKeyResponseDto, getMyApiKey, getMyUser, init, isHttpError, Permission } from '@immich/sdk'; import { convertPathToPattern, glob } from 'fast-glob'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; @@ -34,6 +34,36 @@ export const authenticate = async (options: BaseOptions): Promise => { return auth; }; +export const s = (count: number) => (count === 1 ? '' : 's'); + +let _apiKey: ApiKeyResponseDto; +export const requirePermissions = async (permissions: Permission[]) => { + if (!_apiKey) { + _apiKey = await getMyApiKey(); + } + + if (_apiKey.permissions.includes(Permission.All)) { + return; + } + + const missing: Permission[] = []; + + for (const permission of permissions) { + if (!_apiKey.permissions.includes(permission)) { + missing.push(permission); + } + } + + if (missing.length > 0) { + const combined = missing.map((permission) => `"${permission}"`).join(', '); + console.log( + `Missing required permission${s(missing.length)}: ${combined}. +Please make sure your API key has the correct permissions.`, + ); + process.exit(1); + } +}; + export const connect = async (url: string, key: string) => { const wellKnownUrl = new URL('.well-known/immich', url); try {