From 5a89989a81aa7641e46767c577d166177bda5831 Mon Sep 17 00:00:00 2001 From: Isaac Raja Date: Mon, 21 Sep 2026 16:24:14 +0530 Subject: [PATCH] fix: mirror backend capability checks in scope save guards The scope-profile server functions gated every mutation on the broad assign:configs / manage:configs capabilities. The LibreChat backend is more permissive: whole-doc lifecycle (upsert/toggle/delete) accepts assign:configs:, and field ops (PATCH/DELETE/tombstone) accept manage:configs:
per section. Users holding only scoped grants (e.g. assign:configs:group + manage:configs:balance) were blocked in the panel with "Insufficient permissions: requires one of assign:configs, manage:configs" even though the backend would accept their requests. - whole-doc ops (create/toggle/delete): accept target-scoped assign:configs: in addition to the broad caps - field ops (save/bulk save/remove/tombstone): switch to requireAllSectionCapabilities, which short-circuits on broad manage:configs and otherwise requires manage:configs:
--- src/server/scopes.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/server/scopes.ts b/src/server/scopes.ts index 6e654437..e4be254c 100644 --- a/src/server/scopes.ts +++ b/src/server/scopes.ts @@ -19,7 +19,10 @@ import type * as t from '@/types'; import { isInterfacePermissionPath } from '@/utils/interfacePermissions'; import { stripSecretPreviewValues } from '@/utils'; import { BASE_CONFIG_PRINCIPAL_ID } from './constants'; -import { requireAnyCapability } from './capabilities'; +import { + requireAllSectionCapabilities, + requireAnyCapability, +} from './capabilities'; import { safeFieldPath } from './utils/validation'; import { apiFetch } from './utils/api'; import { @@ -284,11 +287,10 @@ export const saveFieldProfileValueFn = createServerFn({ method: 'POST' }) }), ) .handler(async ({ data }) => { - await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, - SystemCapabilities.MANAGE_CONFIGS, - ]); if (isInterfacePermissionPath(data.fieldPath)) return { success: true }; + // Mirrors the backend field-PATCH check: broad manage:configs or the + // section-scoped manage:configs:
(assign caps do not gate fields). + await requireAllSectionCapabilities([data.fieldPath.split('.')[0]]); const apiType = data.principalType; const entries = await mergeIndexedArrayEntriesForScope(apiType, data.principalId, [ { fieldPath: data.fieldPath, value: data.value }, @@ -338,12 +340,11 @@ export const bulkSaveProfileValuesFn = createServerFn({ method: 'POST' }) entries: Array<{ fieldPath: string; value: unknown }>; }; }) => { - await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, - SystemCapabilities.MANAGE_CONFIGS, - ]); const filtered = data.entries.filter((e) => !isInterfacePermissionPath(e.fieldPath)); if (filtered.length === 0) return { success: true, count: 0 }; + await requireAllSectionCapabilities([ + ...new Set(filtered.map((e) => e.fieldPath.split('.')[0])), + ]); const apiType = data.principalType; const entries = await mergeIndexedArrayEntriesForScope(apiType, data.principalId, filtered); const response = await apiFetch( @@ -396,9 +397,12 @@ export const createScopeFn = createServerFn({ method: 'POST' }) principalId?: string; }; }) => { + // Mirrors the backend upsert check: broad manage, broad assign, or + // target-scoped assign:configs:. await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, SystemCapabilities.MANAGE_CONFIGS, + SystemCapabilities.ASSIGN_CONFIGS, + `assign:configs:${data.principalType}`, ]); const principalId = data.principalId ?? @@ -464,10 +468,7 @@ export const removeFieldProfileValueFn = createServerFn({ method: 'POST' }) }; }) => { if (isInterfacePermissionPath(data.fieldPath)) return { success: true }; - await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, - SystemCapabilities.MANAGE_CONFIGS, - ]); + await requireAllSectionCapabilities([data.fieldPath.split('.')[0]]); const apiType = data.principalType; const response = await apiFetch( `/api/admin/config/${apiType}/${encodeURIComponent(data.principalId)}/fields?fieldPath=${encodeURIComponent(data.fieldPath)}`, @@ -506,10 +507,7 @@ export const tombstoneFieldProfileValueFn = createServerFn({ method: 'POST' }) }; }) => { if (isInterfacePermissionPath(data.fieldPath)) return { success: true }; - await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, - SystemCapabilities.MANAGE_CONFIGS, - ]); + await requireAllSectionCapabilities([data.fieldPath.split('.')[0]]); const apiType = data.principalType; const response = await apiFetch( `/api/admin/config/${apiType}/${encodeURIComponent(data.principalId)}/fields/tombstone`, @@ -551,8 +549,9 @@ export const toggleScopeActiveFn = createServerFn({ method: 'POST' }) }; }) => { await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, SystemCapabilities.MANAGE_CONFIGS, + SystemCapabilities.ASSIGN_CONFIGS, + `assign:configs:${data.principalType}`, ]); const apiType = data.principalType; const response = await apiFetch( @@ -593,8 +592,9 @@ export const deleteScopeFn = createServerFn({ method: 'POST' }) }; }) => { await requireAnyCapability([ - SystemCapabilities.ASSIGN_CONFIGS, SystemCapabilities.MANAGE_CONFIGS, + SystemCapabilities.ASSIGN_CONFIGS, + `assign:configs:${data.principalType}`, ]); const apiType = data.principalType; const response = await apiFetch(