🤏 fix: Filter Admin Config Reads by Section-Scoped Read Capability (#14472)

listConfigs, getBaseConfig, and getConfig only checked the broad
read:configs capability, so a caller holding nothing but
read:configs:<section> grants got a blanket 403 on all three instead
of a response filtered to the sections they hold. Any deployment
using section-scoped config grants hits this.

Adds hasAnyConfigReadAccess as a cheap pre-flight check covering
broad and section-scoped read and manage grants (manage implies
read), so a zero-access caller still 403s before a DB fetch while a
section-scoped caller gets the response filtered to exactly what
they hold. The same manage-implies-read rule is fixed at its root in
getParentCapabilities so a manage-only caller sees the section they
manage instead of having it stripped after passing the pre-flight.

Resolves every section for a request in one batched
getHeldCapabilities query via getReadableConfigSections instead of
one round trip per section.

Includes AppConfig field-renaming normalization (interfaceConfig,
turnstileConfig, mcpConfig) so the filter checks the canonical
section name rather than the renamed response field, and stops
availableTools from bypassing the filter by gating it on its
filteredTools/includedTools source sections.
This commit is contained in:
Dustin Healy 2026-07-28 04:38:37 -07:00 committed by GitHub
parent 728fc1276e
commit 044c134ecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 737 additions and 41 deletions

View file

@ -1,9 +1,22 @@
const { generateCapabilityCheck, capabilityContextMiddleware } = require('@librechat/api');
const { getUserPrincipals, hasCapabilityForPrincipals } = require('~/models');
const { hasCapability, requireCapability, hasConfigCapability } = generateCapabilityCheck({
const {
getUserPrincipals,
hasAnyConfigReadAccess,
hasCapabilityForPrincipals,
getHeldCapabilities,
} = require('~/models');
const {
hasCapability,
requireCapability,
hasConfigCapability,
hasAnyConfigReadAccess: checkAnyConfigReadAccess,
getReadableConfigSections,
} = generateCapabilityCheck({
getUserPrincipals,
hasAnyConfigReadAccess,
hasCapabilityForPrincipals,
getHeldCapabilities,
});
module.exports = {
@ -11,4 +24,6 @@ module.exports = {
requireCapability,
hasConfigCapability,
capabilityContextMiddleware,
hasAnyConfigReadAccess: checkAnyConfigReadAccess,
getReadableConfigSections,
};

View file

@ -3,8 +3,10 @@ const { createAdminConfigHandlers } = require('@librechat/api');
const { SystemCapabilities } = require('@librechat/data-schemas');
const {
hasCapability,
hasConfigCapability,
requireCapability,
hasConfigCapability,
hasAnyConfigReadAccess,
getReadableConfigSections,
} = require('~/server/middleware/roles/capabilities');
const { getAppConfig, invalidateConfigCaches } = require('~/server/services/Config');
const { requireJwtAuth } = require('~/server/middleware');
@ -23,6 +25,8 @@ const handlers = createAdminConfigHandlers({
unsetConfigField: db.unsetConfigField,
deleteConfig: db.deleteConfig,
toggleConfigActive: db.toggleConfigActive,
hasAnyConfigReadAccess,
getReadableConfigSections,
hasConfigCapability,
hasCapability,
getAppConfig,