diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index 85a49bf6c..f3a32993f 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -995,23 +995,4 @@ describe('AppService updating app.locals and issuing warnings', () => { 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); - }); }); diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index a97e48489..b0e2aced9 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -1,6 +1,7 @@ const { SystemRoles, Permissions, + roleDefaults, PermissionTypes, removeNullishValues, } = require('librechat-data-provider'); @@ -8,43 +9,6 @@ const { logger } = require('@librechat/data-schemas'); const { isMemoryEnabled } = require('@librechat/api'); 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 */ @@ -101,6 +65,7 @@ async function loadDefaultInterface(config, configDefaults) { /** @type {TCustomConfig['interface']} */ const loadedInterface = removeNullishValues({ + // UI elements - use schema defaults endpointsMenu: interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu), modelSelect: @@ -112,55 +77,176 @@ async function loadDefaultInterface(config, configDefaults) { privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy, termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService, 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, - peoplePicker: { - users: interfaceConfig?.peoplePicker?.users ?? defaults.peoplePicker?.users, - groups: interfaceConfig?.peoplePicker?.groups ?? defaults.peoplePicker?.groups, - roles: interfaceConfig?.peoplePicker?.roles ?? defaults.peoplePicker?.roles, - }, - marketplace: { - use: interfaceConfig?.marketplace?.use ?? defaults.marketplace?.use, - }, + + // Permissions - only include if explicitly configured + bookmarks: interfaceConfig?.bookmarks, + memories: shouldDisableMemories ? false : interfaceConfig?.memories, + prompts: interfaceConfig?.prompts, + multiConvo: interfaceConfig?.multiConvo, + 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]) { - 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 }, + const defaultPerms = roleDefaults[roleName].permissions; + const existingRole = await getRoleByName(roleName); + const existingPermissions = existingRole?.permissions || {}; + const permissionsToUpdate = {}; + + // Helper to add permission if it should be updated + const addPermissionIfNeeded = (permType, permissions) => { + const permTypeExists = existingPermissions[permType]; + const isExplicitlyConfigured = + interfaceConfig && hasExplicitConfig(interfaceConfig, permType); + + // Only update if: doesn't exist OR explicitly configured + if (!permTypeExists || isExplicitlyConfigured) { + permissionsToUpdate[permType] = permissions; + 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}'`); + } + }; + + // 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; diff --git a/api/server/services/start/interface.spec.js b/api/server/services/start/interface.spec.js index d62439142..02710901c 100644 --- a/api/server/services/start/interface.spec.js +++ b/api/server/services/start/interface.spec.js @@ -1,4 +1,9 @@ -const { SystemRoles, Permissions, PermissionTypes } = require('librechat-data-provider'); +const { + SystemRoles, + Permissions, + PermissionTypes, + roleDefaults, +} = require('librechat-data-provider'); const { updateAccessPermissions, getRoleByName } = require('~/models/Role'); const { loadDefaultInterface } = require('./interface'); @@ -7,6 +12,11 @@ jest.mock('~/models/Role', () => ({ getRoleByName: jest.fn(), })); +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), + isMemoryEnabled: jest.fn((config) => config?.enable === true), +})); + describe('loadDefaultInterface', () => { beforeEach(() => { jest.clearAllMocks(); @@ -41,15 +51,59 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - const expectedPermissions = { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, [Permissions.OPT_OUT]: undefined, }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, + [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 }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, @@ -68,14 +122,14 @@ describe('loadDefaultInterface', () => { // Check USER role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, null, ); // Check ADMIN role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, null, ); }); @@ -107,12 +161,59 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - const expectedPermissions = { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: false }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false }, @@ -131,44 +232,109 @@ describe('loadDefaultInterface', () => { // Check USER role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, null, ); // Check ADMIN role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, null, ); }); - it('should call updateAccessPermissions with undefined when permission types are not specified in config', async () => { + it('should call updateAccessPermissions with role-specific defaults when permission types are not specified in config', async () => { const config = {}; - const configDefaults = { interface: {} }; + 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]: undefined }, - [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, [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_USERS]: undefined, - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_ROLES]: undefined, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, + [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]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, + [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); @@ -176,14 +342,14 @@ describe('loadDefaultInterface', () => { // Check USER role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, null, ); // Check ADMIN role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, null, ); }); @@ -203,24 +369,92 @@ describe('loadDefaultInterface', () => { fileCitations: true, }, }; - const configDefaults = { interface: {} }; + 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 }, + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, - [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, - [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, - [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_USERS]: undefined, - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_ROLES]: undefined, + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: true, + [Permissions.VIEW_ROLES]: true, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, @@ -231,14 +465,14 @@ describe('loadDefaultInterface', () => { // Check USER role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, null, ); // Check ADMIN role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, null, ); }); @@ -270,16 +504,63 @@ describe('loadDefaultInterface', () => { await loadDefaultInterface(config, configDefaults); - const expectedPermissions = { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: 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]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, [PermissionTypes.PEOPLE_PICKER]: { [Permissions.VIEW_USERS]: true, [Permissions.VIEW_GROUPS]: true, @@ -294,14 +575,14 @@ describe('loadDefaultInterface', () => { // Check USER role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, null, ); // Check ADMIN role call expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, null, ); }); @@ -328,24 +609,61 @@ describe('loadDefaultInterface', () => { webSearch: true, fileSearch: true, fileCitations: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: false, + }, }, }; await loadDefaultInterface(config, configDefaults); // Should be called with all permissions EXCEPT prompts and agents (which already exist) - const expectedPermissions = { + const expectedPermissionsForUser = { [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, [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.MARKETPLACE]: { [Permissions.USE]: undefined }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_USERS]: undefined, - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_ROLES]: undefined, + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [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.MARKETPLACE]: { [Permissions.USE]: true }, + [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 }, @@ -354,7 +672,7 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledTimes(2); expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, expect.objectContaining({ permissions: { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, @@ -364,7 +682,7 @@ describe('loadDefaultInterface', () => { ); expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, expect.objectContaining({ permissions: { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, @@ -396,47 +714,472 @@ describe('loadDefaultInterface', () => { prompts: false, agents: true, bookmarks: true, + memories: true, + multiConvo: 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); // Should update prompts (explicitly configured) and all other permissions that don't exist - const expectedPermissions = { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, // Explicitly configured + const expectedPermissionsForUser = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }, // Explicitly configured // All other permissions that don't exist in the database [PermissionTypes.MEMORIES]: { - [Permissions.USE]: undefined, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, [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.MULTI_CONVO]: { [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]: undefined, - [Permissions.VIEW_GROUPS]: undefined, - [Permissions.VIEW_ROLES]: undefined, + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, }, - [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined }, - [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: undefined }, + [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, + [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true }, + }; + + const expectedPermissionsForAdmin = { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }, // Explicitly configured + // All other permissions that don't exist in the database + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }, + [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.MARKETPLACE]: { [Permissions.USE]: true }, + [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); expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.USER, - expectedPermissions, + expectedPermissionsForUser, expect.objectContaining({ permissions: expect.any(Object), }), ); expect(updateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, - expectedPermissions, + expectedPermissionsForAdmin, expect.objectContaining({ permissions: expect.any(Object), }), ); }); + + it('should use role-specific defaults for PEOPLE_PICKER when peoplePicker config is undefined', async () => { + const config = { + interface: { + // peoplePicker is not defined at all + prompts: true, + bookmarks: true, + }, + }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updateAccessPermissions).toHaveBeenCalledTimes(2); + + // Get the calls to updateAccessPermissions + const userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + // For USER role, PEOPLE_PICKER should use USER defaults (false) + expect(userCall[1][PermissionTypes.PEOPLE_PICKER]).toEqual({ + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }); + + // For ADMIN role, PEOPLE_PICKER should use ADMIN defaults (true) + expect(adminCall[1][PermissionTypes.PEOPLE_PICKER]).toEqual({ + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: true, + [Permissions.VIEW_ROLES]: true, + }); + }); + + it('should use role-specific defaults for complex permissions when not configured', async () => { + const config = { + interface: { + // Only configure some permissions, leave others undefined + bookmarks: true, + multiConvo: false, + }, + }; + 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 userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + // Check PROMPTS permissions use role defaults + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }); + + expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }); + + // Check AGENTS permissions use role defaults + expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: false, + [Permissions.CREATE]: true, + }); + + expect(adminCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: true, + [Permissions.CREATE]: true, + }); + + // Check MEMORIES permissions use role defaults + expect(userCall[1][PermissionTypes.MEMORIES]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }); + + expect(adminCall[1][PermissionTypes.MEMORIES]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.UPDATE]: true, + [Permissions.READ]: true, + [Permissions.OPT_OUT]: undefined, + }); + }); + + it('should handle memories OPT_OUT based on personalization when memories are enabled', async () => { + const config = { + interface: { + memories: true, + }, + memory: { + // Memory enabled with personalization + enable: true, + personalize: true, + }, + }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + const userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + // Both roles should have OPT_OUT set to true when personalization is enabled + expect(userCall[1][PermissionTypes.MEMORIES][Permissions.OPT_OUT]).toBe(true); + expect(adminCall[1][PermissionTypes.MEMORIES][Permissions.OPT_OUT]).toBe(true); + }); + + it('should populate missing PEOPLE_PICKER and MARKETPLACE permissions with role-specific defaults', async () => { + // Mock that PEOPLE_PICKER and MARKETPLACE permissions don't exist yet + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + // PEOPLE_PICKER and MARKETPLACE are missing + }, + }); + + const config = { + interface: { + prompts: true, + bookmarks: true, + }, + }; + const configDefaults = { + interface: { + prompts: true, + bookmarks: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: false, + }, + }, + }; + + await loadDefaultInterface(config, configDefaults); + + const userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + // Check that PEOPLE_PICKER uses role-specific defaults from roleDefaults + expect(userCall[1][PermissionTypes.PEOPLE_PICKER]).toEqual({ + [Permissions.VIEW_USERS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_USERS + ], + [Permissions.VIEW_GROUPS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_GROUPS + ], + [Permissions.VIEW_ROLES]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_ROLES + ], + }); + + expect(adminCall[1][PermissionTypes.PEOPLE_PICKER]).toEqual({ + [Permissions.VIEW_USERS]: + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_USERS + ], + [Permissions.VIEW_GROUPS]: + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_GROUPS + ], + [Permissions.VIEW_ROLES]: + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.PEOPLE_PICKER][ + Permissions.VIEW_ROLES + ], + }); + + // Check that MARKETPLACE uses role-specific defaults from roleDefaults + expect(userCall[1][PermissionTypes.MARKETPLACE]).toEqual({ + [Permissions.USE]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.MARKETPLACE][Permissions.USE], + }); + + expect(adminCall[1][PermissionTypes.MARKETPLACE]).toEqual({ + [Permissions.USE]: + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.MARKETPLACE][Permissions.USE], + }); + }); + + it('should leave all existing permissions unchanged when nothing is configured', async () => { + // Mock existing permissions with values that differ from defaults + const existingUserPermissions = { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: true, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: true, + }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: true }, + }; + + getRoleByName.mockResolvedValue({ + permissions: existingUserPermissions, + }); + + // No config provided + const config = undefined; + const configDefaults = { + interface: { + prompts: true, + bookmarks: true, + memories: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: false, + }, + }, + }; + + await loadDefaultInterface(config, configDefaults); + + // Should only update permissions that don't exist + const userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // Should only have permissions for things that don't exist in the role + expect(userCall[1]).not.toHaveProperty(PermissionTypes.PROMPTS); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.BOOKMARKS); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.MEMORIES); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.PEOPLE_PICKER); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.MARKETPLACE); + + // Should have other permissions that weren't in existingUserPermissions + expect(userCall[1]).toHaveProperty(PermissionTypes.MULTI_CONVO); + expect(userCall[1]).toHaveProperty(PermissionTypes.AGENTS); + }); + + it('should only call getRoleByName once per role for efficiency', async () => { + const config = { + interface: { + prompts: true, + bookmarks: true, + }, + }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + // Should call getRoleByName exactly twice (once for USER, once for ADMIN) + expect(getRoleByName).toHaveBeenCalledTimes(2); + expect(getRoleByName).toHaveBeenCalledWith(SystemRoles.USER); + expect(getRoleByName).toHaveBeenCalledWith(SystemRoles.ADMIN); + }); + + it('should only update explicitly configured permissions and leave others unchanged', async () => { + // Mock existing permissions + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: false, + [Permissions.VIEW_GROUPS]: false, + [Permissions.VIEW_ROLES]: false, + }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, + }, + }); + + // Only configure some permissions + const config = { + interface: { + prompts: true, // Explicitly set to true + bookmarks: true, // Explicitly set to true + // memories not configured - should remain unchanged + // peoplePicker not configured - should remain unchanged + marketplace: { + use: true, // Explicitly set to true + }, + }, + }; + const configDefaults = { + interface: { + prompts: false, + bookmarks: false, + memories: true, + peoplePicker: { + users: true, + groups: true, + roles: true, + }, + marketplace: { + use: false, + }, + }, + }; + + await loadDefaultInterface(config, configDefaults); + + const userCall = updateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // Explicitly configured permissions should be updated + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.SHARED_GLOBAL]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS][ + Permissions.SHARED_GLOBAL + ], + [Permissions.CREATE]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS][Permissions.CREATE], + }); + expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); + expect(userCall[1][PermissionTypes.MARKETPLACE]).toEqual({ [Permissions.USE]: true }); + + // Unconfigured permissions should not be present (left unchanged) + expect(userCall[1]).not.toHaveProperty(PermissionTypes.MEMORIES); + expect(userCall[1]).not.toHaveProperty(PermissionTypes.PEOPLE_PICKER); + + // New permissions that didn't exist should still be added + expect(userCall[1]).toHaveProperty(PermissionTypes.AGENTS); + expect(userCall[1]).toHaveProperty(PermissionTypes.MULTI_CONVO); + }); }); diff --git a/packages/data-schemas/src/methods/role.ts b/packages/data-schemas/src/methods/role.ts index 208636f7f..a12c5fafe 100644 --- a/packages/data-schemas/src/methods/role.ts +++ b/packages/data-schemas/src/methods/role.ts @@ -15,14 +15,12 @@ export function createRoleMethods(mongoose: typeof import('mongoose')) { const defaultPerms = roleDefaults[roleName].permissions; if (!role) { - // Create new role if it doesn't exist. role = new Role(roleDefaults[roleName]); } else { - // Ensure role.permissions is defined. + const permissions = role.toObject()?.permissions ?? {}; role.permissions = role.permissions || {}; - // For each permission type in defaults, add it if missing. 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]; } } diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index 6662bd47b..eb16ddd28 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -8,50 +8,50 @@ import type { IRole } from '~/types'; const rolePermissionsSchema = new Schema( { [PermissionTypes.BOOKMARKS]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, - [Permissions.USE]: { type: Boolean, default: true }, - [Permissions.CREATE]: { type: Boolean, default: true }, + [Permissions.SHARED_GLOBAL]: { type: Boolean }, + [Permissions.USE]: { type: Boolean }, + [Permissions.CREATE]: { type: Boolean }, }, [PermissionTypes.MEMORIES]: { - [Permissions.USE]: { type: Boolean, default: true }, - [Permissions.CREATE]: { type: Boolean, default: true }, - [Permissions.UPDATE]: { type: Boolean, default: true }, - [Permissions.READ]: { type: Boolean, default: true }, - [Permissions.OPT_OUT]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, + [Permissions.CREATE]: { type: Boolean }, + [Permissions.UPDATE]: { type: Boolean }, + [Permissions.READ]: { type: Boolean }, + [Permissions.OPT_OUT]: { type: Boolean }, }, [PermissionTypes.AGENTS]: { - [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, - [Permissions.USE]: { type: Boolean, default: true }, - [Permissions.CREATE]: { type: Boolean, default: true }, + [Permissions.SHARED_GLOBAL]: { type: Boolean }, + [Permissions.USE]: { type: Boolean }, + [Permissions.CREATE]: { type: Boolean }, }, [PermissionTypes.MULTI_CONVO]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.TEMPORARY_CHAT]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.RUN_CODE]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.WEB_SEARCH]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.PEOPLE_PICKER]: { - [Permissions.VIEW_USERS]: { type: Boolean, default: false }, - [Permissions.VIEW_GROUPS]: { type: Boolean, default: false }, - [Permissions.VIEW_ROLES]: { type: Boolean, default: false }, + [Permissions.VIEW_USERS]: { type: Boolean }, + [Permissions.VIEW_GROUPS]: { type: Boolean }, + [Permissions.VIEW_ROLES]: { type: Boolean }, }, [PermissionTypes.MARKETPLACE]: { - [Permissions.USE]: { type: Boolean, default: false }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.FILE_SEARCH]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.FILE_CITATIONS]: { - [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.USE]: { type: Boolean }, }, }, { _id: false }, @@ -61,37 +61,6 @@ const roleSchema: Schema = new Schema({ name: { type: String, required: true, unique: true, index: true }, permissions: { 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 }, - }), }, });