From 53c31b85d011a79b391d20df022c0d028e0baf1a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 10 Aug 2025 21:42:54 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=9E=EF=B8=8F=20refactor:=20Apply=20Rol?= =?UTF-8?q?e=20Permissions=20at=20Startup=20only=20if=20Missing=20or=20Con?= =?UTF-8?q?figured?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/Role.js | 8 +- .../services/AppService.interface.spec.js | 2 +- api/server/services/AppService.spec.js | 1 + api/server/services/start/interface.js | 154 +++-- api/server/services/start/interface.spec.js | 622 +++++++----------- api/typedefs.js | 6 + client/src/locales/en/translation.json | 2 - 7 files changed, 355 insertions(+), 440 deletions(-) diff --git a/api/models/Role.js b/api/models/Role.js index 8f9e8810f..1766dc9b0 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -16,7 +16,7 @@ const { Role } = require('~/db/models'); * * @param {string} roleName - The name of the role to find or create. * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} A plain object representing the role document. + * @returns {Promise} Role document. */ const getRoleByName = async function (roleName, fieldsToSelect = null) { const cache = getLogStores(CacheKeys.ROLES); @@ -72,8 +72,9 @@ const updateRoleByName = async function (roleName, updates) { * Updates access permissions for a specific role and multiple permission types. * @param {string} roleName - The role to update. * @param {Object.>} permissionsUpdate - Permissions to update and their values. + * @param {IRole} [roleData] - Optional role data to use instead of fetching from the database. */ -async function updateAccessPermissions(roleName, permissionsUpdate) { +async function updateAccessPermissions(roleName, permissionsUpdate, roleData) { // Filter and clean the permission updates based on our schema definition. const updates = {}; for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) { @@ -86,7 +87,7 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { } try { - const role = await getRoleByName(roleName); + const role = roleData ?? (await getRoleByName(roleName)); if (!role) { return; } @@ -113,7 +114,6 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { } } - // Process the current updates for (const [permissionType, permissions] of Object.entries(updates)) { const currentTypePermissions = currentPermissions[permissionType] || {}; updatedPermissions[permissionType] = { ...currentTypePermissions }; diff --git a/api/server/services/AppService.interface.spec.js b/api/server/services/AppService.interface.spec.js index 81ebec60d..31c8d70f3 100644 --- a/api/server/services/AppService.interface.spec.js +++ b/api/server/services/AppService.interface.spec.js @@ -5,7 +5,7 @@ jest.mock('~/models', () => ({ })); jest.mock('~/models/Role', () => ({ updateAccessPermissions: jest.fn(), - getRoleByName: jest.fn(), + getRoleByName: jest.fn().mockResolvedValue(null), updateRoleByName: jest.fn(), })); diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index db53af1b2..85a49bf6c 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -33,6 +33,7 @@ jest.mock('~/models', () => ({ })); jest.mock('~/models/Role', () => ({ updateAccessPermissions: jest.fn(), + getRoleByName: jest.fn().mockResolvedValue(null), })); jest.mock('./Config', () => ({ setCachedTools: jest.fn(), diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 3e892138a..a97e48489 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -6,16 +6,86 @@ const { } = require('librechat-data-provider'); const { logger } = require('@librechat/data-schemas'); const { isMemoryEnabled } = require('@librechat/api'); -const { updateAccessPermissions } = 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 + */ +function hasExplicitConfig(interfaceConfig, permissionType) { + switch (permissionType) { + case PermissionTypes.PROMPTS: + return interfaceConfig.prompts !== undefined; + case PermissionTypes.BOOKMARKS: + return interfaceConfig.bookmarks !== undefined; + case PermissionTypes.MEMORIES: + return interfaceConfig.memories !== undefined; + case PermissionTypes.MULTI_CONVO: + return interfaceConfig.multiConvo !== undefined; + case PermissionTypes.AGENTS: + return interfaceConfig.agents !== undefined; + case PermissionTypes.TEMPORARY_CHAT: + return interfaceConfig.temporaryChat !== undefined; + case PermissionTypes.RUN_CODE: + return interfaceConfig.runCode !== undefined; + case PermissionTypes.WEB_SEARCH: + return interfaceConfig.webSearch !== undefined; + case PermissionTypes.PEOPLE_PICKER: + return interfaceConfig.peoplePicker !== undefined; + case PermissionTypes.MARKETPLACE: + return interfaceConfig.marketplace !== undefined; + case PermissionTypes.FILE_SEARCH: + return interfaceConfig.fileSearch !== undefined; + case PermissionTypes.FILE_CITATIONS: + return interfaceConfig.fileCitations !== undefined; + default: + return false; + } +} /** * Loads the default interface object. * @param {TCustomConfig | undefined} config - The loaded custom configuration. * @param {TConfigDefaults} configDefaults - The custom configuration default values. - * @param {SystemRoles} [roleName] - The role to load the default interface for, defaults to `'USER'`. * @returns {Promise} The default interface object. */ -async function loadDefaultInterface(config, configDefaults, roleName = SystemRoles.USER) { +async function loadDefaultInterface(config, configDefaults) { const { interface: interfaceConfig } = config ?? {}; const { interface: defaults } = configDefaults; const hasModelSpecs = config?.modelSpecs?.list?.length > 0; @@ -63,55 +133,35 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol }, }); - await updateAccessPermissions(roleName, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: loadedInterface.memories, - [Permissions.OPT_OUT]: isPersonalizationEnabled, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_USERS]: - roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.users, - [Permissions.VIEW_GROUPS]: - roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.groups, - [Permissions.VIEW_ROLES]: - roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.roles, - }, - [PermissionTypes.MARKETPLACE]: { - [Permissions.USE]: roleName === SystemRoles.USER ? false : loadedInterface.marketplace?.use, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations }, - }); - await updateAccessPermissions(SystemRoles.ADMIN, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: loadedInterface.memories, - [Permissions.OPT_OUT]: isPersonalizationEnabled, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_USERS]: loadedInterface.peoplePicker?.users, - [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker?.groups, - [Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles, - }, - [PermissionTypes.MARKETPLACE]: { - [Permissions.USE]: loadedInterface.marketplace?.use, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations }, - }); + for (const roleName of [SystemRoles.USER, SystemRoles.ADMIN]) { + await updateRolePermissions({ + roleName, + allPermissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: loadedInterface.memories, + [Permissions.OPT_OUT]: isPersonalizationEnabled, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: loadedInterface.peoplePicker?.users, + [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker?.groups, + [Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles, + }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: loadedInterface.marketplace?.use, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations }, + }, + interfaceConfig, + }); + } let i = 0; const logSettings = () => { diff --git a/api/server/services/start/interface.spec.js b/api/server/services/start/interface.spec.js index d9f3d4e06..d62439142 100644 --- a/api/server/services/start/interface.spec.js +++ b/api/server/services/start/interface.spec.js @@ -1,12 +1,19 @@ const { SystemRoles, Permissions, PermissionTypes } = require('librechat-data-provider'); -const { updateAccessPermissions } = require('~/models/Role'); +const { updateAccessPermissions, getRoleByName } = require('~/models/Role'); const { loadDefaultInterface } = require('./interface'); jest.mock('~/models/Role', () => ({ updateAccessPermissions: jest.fn(), + getRoleByName: jest.fn(), })); describe('loadDefaultInterface', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Mock getRoleByName to return null (no existing permissions) + getRoleByName.mockResolvedValue(null); + }); + it('should call updateAccessPermissions with the correct parameters when permission types are true', async () => { const config = { interface: { @@ -20,13 +27,21 @@ describe('loadDefaultInterface', () => { webSearch: true, fileSearch: true, fileCitations: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: true, + }, }, }; const configDefaults = { interface: {} }; await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { + const expectedPermissions = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -38,14 +53,31 @@ describe('loadDefaultInterface', () => { [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: true, + [Permissions.VIEW_ROLES]: true, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, - }); + }; + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + + // Check USER role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + null, + ); + + // Check ADMIN role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + null, + ); }); it('should call updateAccessPermissions with false when permission types are false', async () => { @@ -61,13 +93,21 @@ describe('loadDefaultInterface', () => { webSearch: false, fileSearch: false, fileCitations: false, + peoplePicker: { + users: false, + groups: false, + roles: false, + }, + marketplace: { + use: false, + }, }, }; const configDefaults = { interface: {} }; await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { + const expectedPermissions = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined }, @@ -76,14 +116,31 @@ describe('loadDefaultInterface', () => { [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: false }, - }); + }; + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + + // Check USER role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + null, + ); + + // Check ADMIN role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + null, + ); }); it('should call updateAccessPermissions with undefined when permission types are not specified in config', async () => { @@ -92,7 +149,7 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { + const expectedPermissions = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, [PermissionTypes.MEMORIES]: { @@ -106,52 +163,29 @@ describe('loadDefaultInterface', () => { [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_GROUPS]: undefined, + [Permissions.VIEW_ROLES]: undefined, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with undefined when permission types are explicitly undefined', async () => { - const config = { - interface: { - prompts: undefined, - bookmarks: undefined, - memories: undefined, - multiConvo: undefined, - agents: undefined, - temporaryChat: undefined, - runCode: undefined, - webSearch: undefined, - fileSearch: undefined, - }, }; - const configDefaults = { interface: {} }; - await loadDefaultInterface(config, configDefaults); + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); + // Check USER role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + null, + ); + + // Check ADMIN role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + null, + ); }); it('should call updateAccessPermissions with mixed values for permission types', async () => { @@ -173,7 +207,7 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { + const expectedPermissions = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, @@ -184,15 +218,103 @@ describe('loadDefaultInterface', () => { [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_GROUPS]: undefined, + [Permissions.VIEW_ROLES]: undefined, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, - }); + }; + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + + // Check USER role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + null, + ); + + // Check ADMIN role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + null, + ); }); - it('should call updateAccessPermissions with true when config is undefined', async () => { + it('should use default values when config is undefined', async () => { + const config = undefined; + const configDefaults = { + interface: { + prompts: true, + bookmarks: true, + memories: true, + multiConvo: true, + agents: true, + temporaryChat: true, + runCode: true, + webSearch: true, + fileSearch: true, + fileCitations: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: false, + }, + }, + }; + + await loadDefaultInterface(config, configDefaults); + + const expectedPermissions = { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: true, + [Permissions.VIEW_ROLES]: true, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + + // Check USER role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + null, + ); + + // Check ADMIN role call + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + null, + ); + }); + + it('should only update permissions that do not exist when no config provided', async () => { + // Mock that some permissions already exist + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + }, + }); + const config = undefined; const configDefaults = { interface: { @@ -211,372 +333,110 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + // Should be called with all permissions EXCEPT prompts and agents (which already exist) + const expectedPermissions = { [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_GROUPS]: undefined, + [Permissions.VIEW_ROLES]: undefined, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, - }); + }; + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + expect.objectContaining({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + }, + }), + ); + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + expect.objectContaining({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + }, + }), + ); }); - it('should call updateAccessPermissions with the correct parameters when multiConvo is true', async () => { - const config = { interface: { multiConvo: true } }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, + it('should override existing permissions when explicitly configured', async () => { + // Mock that some permissions already exist + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, }); - }); - it('should call updateAccessPermissions with false when multiConvo is false', async () => { - const config = { interface: { multiConvo: false } }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with undefined when multiConvo is not specified in config', async () => { - const config = {}; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with all interface options including multiConvo', async () => { const config = { interface: { - prompts: true, - bookmarks: false, - memories: true, - multiConvo: true, - agents: false, - temporaryChat: true, - runCode: false, - fileSearch: true, + prompts: true, // Explicitly set, should override existing false + // agents not specified, so existing false should be preserved + // bookmarks not specified, so existing false should be preserved }, }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should use default values for multiConvo when config is undefined', async () => { - const config = undefined; const configDefaults = { interface: { - prompts: true, + prompts: false, + agents: true, bookmarks: true, - memories: false, - multiConvo: false, - agents: undefined, - temporaryChat: undefined, - runCode: undefined, - webSearch: undefined, - fileSearch: true, }, }; await loadDefaultInterface(config, configDefaults); - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, + // Should update prompts (explicitly configured) and all other permissions that don't exist + const expectedPermissions = { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, // Explicitly configured + // All other permissions that don't exist in the database + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: undefined, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with the correct parameters when WEB_SEARCH is undefined', async () => { - const config = { - interface: { - prompts: true, - bookmarks: false, - memories: true, - multiConvo: true, - agents: false, - temporaryChat: true, - runCode: false, - fileCitations: true, - }, - }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, + [Permissions.VIEW_ROLES]: undefined, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, - }); - }); - - it('should call updateAccessPermissions with the correct parameters when FILE_SEARCH is true', async () => { - const config = { - interface: { - fileSearch: true, - }, - }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with false when FILE_SEARCH is false', async () => { - const config = { - interface: { - fileSearch: false, - }, }; - const configDefaults = { interface: {} }; - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with all interface options including fileSearch', async () => { - const config = { - interface: { - prompts: true, - bookmarks: false, - memories: true, - multiConvo: true, - agents: false, - temporaryChat: true, - runCode: false, - webSearch: true, - fileSearch: true, - }, - }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with the correct parameters when fileCitations is true', async () => { - const config = { interface: { fileCitations: true } }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - }); - }); - - it('should call updateAccessPermissions with false when fileCitations is false', async () => { - const config = { interface: { fileCitations: false } }; - const configDefaults = { interface: {} }; - - await loadDefaultInterface(config, configDefaults); - - expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, - [Permissions.OPT_OUT]: undefined, - }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, - [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, - [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: false }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, - [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_USERS]: undefined, - }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - }); + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.USER, + expectedPermissions, + expect.objectContaining({ + permissions: expect.any(Object), + }), + ); + expect(updateAccessPermissions).toHaveBeenCalledWith( + SystemRoles.ADMIN, + expectedPermissions, + expect.objectContaining({ + permissions: expect.any(Object), + }), + ); }); }); diff --git a/api/typedefs.js b/api/typedefs.js index deaa210e8..b8d2aa348 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -888,6 +888,12 @@ * @memberof typedefs */ +/** + * @exports IRole + * @typedef {import('@librechat/data-schemas').IRole} IRole + * @memberof typedefs + */ + /** * @exports ObjectId * @typedef {import('mongoose').Types.ObjectId} ObjectId diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 79a0e720c..e96436879 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -594,8 +594,6 @@ "com_ui_people_picker_allow_view_roles": "Allow viewing roles", "com_ui_marketplace": "Marketplace", "com_ui_marketplace_allow_use": "Allow using Marketplace", - "com_ui_marketplace_admin_settings": "Marketplace Admin Settings", - "com_ui_marketplace_admin_settings_description": "Configure which roles can access the Agent Marketplace.", "com_ui_all": "all", "com_ui_all_proper": "All", "com_ui_analyzing": "Analyzing",