🎏 refactor: Streamline Role Permissions from Interface Config

This commit is contained in:
Danny Avila 2025-08-14 02:15:33 -04:00
parent b742c8c7f9
commit e8ddd279fd
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
5 changed files with 1008 additions and 231 deletions

View file

@ -995,23 +995,4 @@ describe('AppService updating app.locals and issuing warnings', () => {
roles: true, roles: true,
}); });
}); });
it('should use default peoplePicker permissions when not specified', async () => {
const mockConfig = {
interface: {
// No peoplePicker configuration
},
};
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
const app = { locals: {} };
await AppService(app);
// Check that default permissions are applied
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
});
}); });

View file

@ -1,6 +1,7 @@
const { const {
SystemRoles, SystemRoles,
Permissions, Permissions,
roleDefaults,
PermissionTypes, PermissionTypes,
removeNullishValues, removeNullishValues,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
@ -8,43 +9,6 @@ const { logger } = require('@librechat/data-schemas');
const { isMemoryEnabled } = require('@librechat/api'); const { isMemoryEnabled } = require('@librechat/api');
const { updateAccessPermissions, getRoleByName } = require('~/models/Role'); const { updateAccessPermissions, getRoleByName } = require('~/models/Role');
/**
* Updates role permissions intelligently - only updates permission types that:
* 1. Don't exist in the database (first time setup)
* 2. Are explicitly configured in the config file
* @param {object} params - The role name to update
* @param {string} params.roleName - The role name to update
* @param {object} params.allPermissions - All permissions to potentially update
* @param {object} params.interfaceConfig - The interface config from librechat.yaml
*/
async function updateRolePermissions({ roleName, allPermissions, interfaceConfig }) {
const existingRole = await getRoleByName(roleName);
const existingPermissions = existingRole?.permissions || {};
const permissionsToUpdate = {};
for (const [permType, perms] of Object.entries(allPermissions)) {
const permTypeExists = existingPermissions[permType];
const isExplicitlyConfigured = interfaceConfig && hasExplicitConfig(interfaceConfig, permType);
// Only update if: doesn't exist OR explicitly configured
if (!permTypeExists || isExplicitlyConfigured) {
permissionsToUpdate[permType] = perms;
if (!permTypeExists) {
logger.debug(`Role '${roleName}': Setting up default permissions for '${permType}'`);
} else if (isExplicitlyConfigured) {
logger.debug(`Role '${roleName}': Applying explicit config for '${permType}'`);
}
} else {
logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`);
}
}
if (Object.keys(permissionsToUpdate).length > 0) {
await updateAccessPermissions(roleName, permissionsToUpdate, existingRole);
}
}
/** /**
* Checks if a permission type has explicit configuration * Checks if a permission type has explicit configuration
*/ */
@ -101,6 +65,7 @@ async function loadDefaultInterface(config, configDefaults) {
/** @type {TCustomConfig['interface']} */ /** @type {TCustomConfig['interface']} */
const loadedInterface = removeNullishValues({ const loadedInterface = removeNullishValues({
// UI elements - use schema defaults
endpointsMenu: endpointsMenu:
interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu), interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu),
modelSelect: modelSelect:
@ -112,55 +77,176 @@ async function loadDefaultInterface(config, configDefaults) {
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy, privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService, termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers, mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers,
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
memories: shouldDisableMemories ? false : (interfaceConfig?.memories ?? defaults.memories),
prompts: interfaceConfig?.prompts ?? defaults.prompts,
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
agents: interfaceConfig?.agents ?? defaults.agents,
temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat,
runCode: interfaceConfig?.runCode ?? defaults.runCode,
webSearch: interfaceConfig?.webSearch ?? defaults.webSearch,
fileSearch: interfaceConfig?.fileSearch ?? defaults.fileSearch,
fileCitations: interfaceConfig?.fileCitations ?? defaults.fileCitations,
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome, customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
peoplePicker: {
users: interfaceConfig?.peoplePicker?.users ?? defaults.peoplePicker?.users, // Permissions - only include if explicitly configured
groups: interfaceConfig?.peoplePicker?.groups ?? defaults.peoplePicker?.groups, bookmarks: interfaceConfig?.bookmarks,
roles: interfaceConfig?.peoplePicker?.roles ?? defaults.peoplePicker?.roles, memories: shouldDisableMemories ? false : interfaceConfig?.memories,
}, prompts: interfaceConfig?.prompts,
marketplace: { multiConvo: interfaceConfig?.multiConvo,
use: interfaceConfig?.marketplace?.use ?? defaults.marketplace?.use, agents: interfaceConfig?.agents,
}, temporaryChat: interfaceConfig?.temporaryChat,
runCode: interfaceConfig?.runCode,
webSearch: interfaceConfig?.webSearch,
fileSearch: interfaceConfig?.fileSearch,
fileCitations: interfaceConfig?.fileCitations,
peoplePicker: interfaceConfig?.peoplePicker,
marketplace: interfaceConfig?.marketplace,
}); });
// Helper to get permission value with proper precedence
const getPermissionValue = (configValue, roleDefault, schemaDefault) => {
if (configValue !== undefined) return configValue;
if (roleDefault !== undefined) return roleDefault;
return schemaDefault;
};
// Permission precedence order:
// 1. Explicit user configuration (from librechat.yaml)
// 2. Role-specific defaults (from roleDefaults)
// 3. Interface schema defaults (from interfaceSchema.default())
for (const roleName of [SystemRoles.USER, SystemRoles.ADMIN]) { for (const roleName of [SystemRoles.USER, SystemRoles.ADMIN]) {
await updateRolePermissions({ const defaultPerms = roleDefaults[roleName].permissions;
roleName, const existingRole = await getRoleByName(roleName);
allPermissions: { const existingPermissions = existingRole?.permissions || {};
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, const permissionsToUpdate = {};
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
[PermissionTypes.MEMORIES]: { // Helper to add permission if it should be updated
[Permissions.USE]: loadedInterface.memories, const addPermissionIfNeeded = (permType, permissions) => {
[Permissions.OPT_OUT]: isPersonalizationEnabled, const permTypeExists = existingPermissions[permType];
}, const isExplicitlyConfigured =
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, interfaceConfig && hasExplicitConfig(interfaceConfig, permType);
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, // Only update if: doesn't exist OR explicitly configured
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, if (!permTypeExists || isExplicitlyConfigured) {
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, permissionsToUpdate[permType] = permissions;
[PermissionTypes.PEOPLE_PICKER]: { if (!permTypeExists) {
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker?.users, logger.debug(`Role '${roleName}': Setting up default permissions for '${permType}'`);
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker?.groups, } else if (isExplicitlyConfigured) {
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles, logger.debug(`Role '${roleName}': Applying explicit config for '${permType}'`);
}, }
[PermissionTypes.MARKETPLACE]: { } else {
[Permissions.USE]: loadedInterface.marketplace?.use, logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`);
}, }
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, };
[PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations },
// Build permissions for each type
const allPermissions = {
[PermissionTypes.PROMPTS]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.prompts,
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.USE],
defaults.prompts,
),
[Permissions.SHARED_GLOBAL]:
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARED_GLOBAL],
[Permissions.CREATE]: defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE],
}, },
interfaceConfig, [PermissionTypes.BOOKMARKS]: {
}); [Permissions.USE]: getPermissionValue(
loadedInterface.bookmarks,
defaultPerms[PermissionTypes.BOOKMARKS]?.[Permissions.USE],
defaults.bookmarks,
),
},
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.memories,
defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.USE],
defaults.memories,
),
[Permissions.CREATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.CREATE],
[Permissions.UPDATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.UPDATE],
[Permissions.READ]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.READ],
[Permissions.OPT_OUT]: isPersonalizationEnabled,
},
[PermissionTypes.MULTI_CONVO]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.multiConvo,
defaultPerms[PermissionTypes.MULTI_CONVO]?.[Permissions.USE],
defaults.multiConvo,
),
},
[PermissionTypes.AGENTS]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.agents,
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.USE],
defaults.agents,
),
[Permissions.SHARED_GLOBAL]:
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARED_GLOBAL],
[Permissions.CREATE]: defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE],
},
[PermissionTypes.TEMPORARY_CHAT]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.temporaryChat,
defaultPerms[PermissionTypes.TEMPORARY_CHAT]?.[Permissions.USE],
defaults.temporaryChat,
),
},
[PermissionTypes.RUN_CODE]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.runCode,
defaultPerms[PermissionTypes.RUN_CODE]?.[Permissions.USE],
defaults.runCode,
),
},
[PermissionTypes.WEB_SEARCH]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.webSearch,
defaultPerms[PermissionTypes.WEB_SEARCH]?.[Permissions.USE],
defaults.webSearch,
),
},
[PermissionTypes.PEOPLE_PICKER]: {
[Permissions.VIEW_USERS]: getPermissionValue(
loadedInterface.peoplePicker?.users,
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_USERS],
defaults.peoplePicker?.users,
),
[Permissions.VIEW_GROUPS]: getPermissionValue(
loadedInterface.peoplePicker?.groups,
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_GROUPS],
defaults.peoplePicker?.groups,
),
[Permissions.VIEW_ROLES]: getPermissionValue(
loadedInterface.peoplePicker?.roles,
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_ROLES],
defaults.peoplePicker?.roles,
),
},
[PermissionTypes.MARKETPLACE]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.marketplace?.use,
defaultPerms[PermissionTypes.MARKETPLACE]?.[Permissions.USE],
defaults.marketplace?.use,
),
},
[PermissionTypes.FILE_SEARCH]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.fileSearch,
defaultPerms[PermissionTypes.FILE_SEARCH]?.[Permissions.USE],
defaults.fileSearch,
),
},
[PermissionTypes.FILE_CITATIONS]: {
[Permissions.USE]: getPermissionValue(
loadedInterface.fileCitations,
defaultPerms[PermissionTypes.FILE_CITATIONS]?.[Permissions.USE],
defaults.fileCitations,
),
},
};
// Check and add each permission type if needed
for (const [permType, permissions] of Object.entries(allPermissions)) {
addPermissionIfNeeded(permType, permissions);
}
// Update permissions if any need updating
if (Object.keys(permissionsToUpdate).length > 0) {
await updateAccessPermissions(roleName, permissionsToUpdate, existingRole);
}
} }
let i = 0; let i = 0;

File diff suppressed because it is too large Load diff

View file

@ -15,14 +15,12 @@ export function createRoleMethods(mongoose: typeof import('mongoose')) {
const defaultPerms = roleDefaults[roleName].permissions; const defaultPerms = roleDefaults[roleName].permissions;
if (!role) { if (!role) {
// Create new role if it doesn't exist.
role = new Role(roleDefaults[roleName]); role = new Role(roleDefaults[roleName]);
} else { } else {
// Ensure role.permissions is defined. const permissions = role.toObject()?.permissions ?? {};
role.permissions = role.permissions || {}; role.permissions = role.permissions || {};
// For each permission type in defaults, add it if missing.
for (const permType of Object.keys(defaultPerms)) { for (const permType of Object.keys(defaultPerms)) {
if (role.permissions[permType] == null) { if (permissions[permType] == null || Object.keys(permissions[permType]).length === 0) {
role.permissions[permType] = defaultPerms[permType as keyof typeof defaultPerms]; role.permissions[permType] = defaultPerms[permType as keyof typeof defaultPerms];
} }
} }

View file

@ -8,50 +8,50 @@ import type { IRole } from '~/types';
const rolePermissionsSchema = new Schema( const rolePermissionsSchema = new Schema(
{ {
[PermissionTypes.BOOKMARKS]: { [PermissionTypes.BOOKMARKS]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.PROMPTS]: { [PermissionTypes.PROMPTS]: {
[Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, [Permissions.SHARED_GLOBAL]: { type: Boolean },
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
[Permissions.CREATE]: { type: Boolean, default: true }, [Permissions.CREATE]: { type: Boolean },
}, },
[PermissionTypes.MEMORIES]: { [PermissionTypes.MEMORIES]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
[Permissions.CREATE]: { type: Boolean, default: true }, [Permissions.CREATE]: { type: Boolean },
[Permissions.UPDATE]: { type: Boolean, default: true }, [Permissions.UPDATE]: { type: Boolean },
[Permissions.READ]: { type: Boolean, default: true }, [Permissions.READ]: { type: Boolean },
[Permissions.OPT_OUT]: { type: Boolean, default: true }, [Permissions.OPT_OUT]: { type: Boolean },
}, },
[PermissionTypes.AGENTS]: { [PermissionTypes.AGENTS]: {
[Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, [Permissions.SHARED_GLOBAL]: { type: Boolean },
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
[Permissions.CREATE]: { type: Boolean, default: true }, [Permissions.CREATE]: { type: Boolean },
}, },
[PermissionTypes.MULTI_CONVO]: { [PermissionTypes.MULTI_CONVO]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.TEMPORARY_CHAT]: { [PermissionTypes.TEMPORARY_CHAT]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.RUN_CODE]: { [PermissionTypes.RUN_CODE]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.WEB_SEARCH]: { [PermissionTypes.WEB_SEARCH]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.PEOPLE_PICKER]: { [PermissionTypes.PEOPLE_PICKER]: {
[Permissions.VIEW_USERS]: { type: Boolean, default: false }, [Permissions.VIEW_USERS]: { type: Boolean },
[Permissions.VIEW_GROUPS]: { type: Boolean, default: false }, [Permissions.VIEW_GROUPS]: { type: Boolean },
[Permissions.VIEW_ROLES]: { type: Boolean, default: false }, [Permissions.VIEW_ROLES]: { type: Boolean },
}, },
[PermissionTypes.MARKETPLACE]: { [PermissionTypes.MARKETPLACE]: {
[Permissions.USE]: { type: Boolean, default: false }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.FILE_SEARCH]: { [PermissionTypes.FILE_SEARCH]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
[PermissionTypes.FILE_CITATIONS]: { [PermissionTypes.FILE_CITATIONS]: {
[Permissions.USE]: { type: Boolean, default: true }, [Permissions.USE]: { type: Boolean },
}, },
}, },
{ _id: false }, { _id: false },
@ -61,37 +61,6 @@ const roleSchema: Schema<IRole> = new Schema({
name: { type: String, required: true, unique: true, index: true }, name: { type: String, required: true, unique: true, index: true },
permissions: { permissions: {
type: rolePermissionsSchema, type: rolePermissionsSchema,
default: () => ({
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.PROMPTS]: {
[Permissions.SHARED_GLOBAL]: false,
[Permissions.USE]: true,
[Permissions.CREATE]: true,
},
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: true,
[Permissions.CREATE]: true,
[Permissions.UPDATE]: true,
[Permissions.READ]: true,
},
[PermissionTypes.AGENTS]: {
[Permissions.SHARED_GLOBAL]: false,
[Permissions.USE]: true,
[Permissions.CREATE]: true,
},
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.PEOPLE_PICKER]: {
[Permissions.VIEW_USERS]: false,
[Permissions.VIEW_GROUPS]: false,
[Permissions.VIEW_ROLES]: false,
},
[PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true },
}),
}, },
}); });