diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 5ac2645c04..d108844d20 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -372,52 +372,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(agentInDb.id).toBe(existingAgentId); }); - test('should reject update from non-author when not collaborative', async () => { - const differentUserId = new mongoose.Types.ObjectId().toString(); - mockReq.user.id = differentUserId; // Different user - mockReq.params.id = existingAgentId; - mockReq.body = { - name: 'Unauthorized Update', - }; - - await updateAgentHandler(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(403); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'You do not have permission to modify this non-collaborative agent', - }); - - // Verify agent was not modified in database - const agentInDb = await Agent.findOne({ id: existingAgentId }); - expect(agentInDb.name).toBe('Original Agent'); - }); - - test('should allow update from non-author when collaborative', async () => { - // First make the agent collaborative - await Agent.updateOne({ id: existingAgentId }, { isCollaborative: true }); - - const differentUserId = new mongoose.Types.ObjectId().toString(); - mockReq.user.id = differentUserId; // Different user - mockReq.params.id = existingAgentId; - mockReq.body = { - name: 'Collaborative Update', - }; - - await updateAgentHandler(mockReq, mockRes); - - expect(mockRes.status).not.toHaveBeenCalledWith(403); - expect(mockRes.json).toHaveBeenCalled(); - - const updatedAgent = mockRes.json.mock.calls[0][0]; - expect(updatedAgent.name).toBe('Collaborative Update'); - // Author field should be removed for non-author - expect(updatedAgent.author).toBeUndefined(); - - // Verify in database - const agentInDb = await Agent.findOne({ id: existingAgentId }); - expect(agentInDb.name).toBe('Collaborative Update'); - }); - test('should allow admin to update any agent', async () => { const adminUserId = new mongoose.Types.ObjectId().toString(); mockReq.user.id = adminUserId; @@ -555,45 +509,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(agentInDb.__v).not.toBe(99); }); - test('should prevent privilege escalation through isCollaborative', async () => { - // Create a non-collaborative agent - const authorId = new mongoose.Types.ObjectId(); - const agent = await Agent.create({ - id: `agent_${uuidv4()}`, - name: 'Private Agent', - provider: 'openai', - model: 'gpt-4', - author: authorId, - isCollaborative: false, - versions: [ - { - name: 'Private Agent', - provider: 'openai', - model: 'gpt-4', - createdAt: new Date(), - updatedAt: new Date(), - }, - ], - }); - - // Try to make it collaborative as a different user - const attackerId = new mongoose.Types.ObjectId().toString(); - mockReq.user.id = attackerId; - mockReq.params.id = agent.id; - mockReq.body = { - isCollaborative: true, // Trying to escalate privileges - }; - - await updateAgentHandler(mockReq, mockRes); - - // Should be rejected - expect(mockRes.status).toHaveBeenCalledWith(403); - - // Verify in database that it's still not collaborative - const agentInDb = await Agent.findOne({ id: agent.id }); - expect(agentInDb.isCollaborative).toBe(false); - }); - test('should prevent author hijacking', async () => { const originalAuthorId = new mongoose.Types.ObjectId(); const attackerId = new mongoose.Types.ObjectId(); diff --git a/api/server/middleware/roles/access.spec.js b/api/server/middleware/roles/access.spec.js index 66ae5e0fe2..fe8d77a4f5 100644 --- a/api/server/middleware/roles/access.spec.js +++ b/api/server/middleware/roles/access.spec.js @@ -165,7 +165,8 @@ describe('Access Middleware', () => { expect(result).toBe(false); }); - test('should return true if user has any of multiple permissions', async () => { + test('should return false if user has only some of multiple permissions', async () => { + // User has USE but not CREATE, so should fail when checking for both const result = await checkAccess({ req: {}, user: { id: 'user123', role: 'user' }, @@ -173,6 +174,18 @@ describe('Access Middleware', () => { permissions: [Permissions.CREATE, Permissions.USE], getRoleByName, }); + expect(result).toBe(false); + }); + + test('should return true if user has all of multiple permissions', async () => { + // Admin has both USE and CREATE + const result = await checkAccess({ + req: {}, + user: { id: 'admin123', role: 'admin' }, + permissionType: PermissionTypes.AGENTS, + permissions: [Permissions.CREATE, Permissions.USE], + getRoleByName, + }); expect(result).toBe(true); }); diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 2fb0748b9e..a486147a92 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -53,20 +53,20 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome, peoplePicker: { admin: { - users: interfaceConfig?.peoplePicker?.admin?.users ?? defaults.peoplePicker.admin.users, - groups: interfaceConfig?.peoplePicker?.admin?.groups ?? defaults.peoplePicker.admin.groups, + users: interfaceConfig?.peoplePicker?.admin?.users ?? defaults.peoplePicker?.admin.users, + groups: interfaceConfig?.peoplePicker?.admin?.groups ?? defaults.peoplePicker?.admin.groups, }, user: { - users: interfaceConfig?.peoplePicker?.user?.users ?? defaults.peoplePicker.user.users, - groups: interfaceConfig?.peoplePicker?.user?.groups ?? defaults.peoplePicker.user.groups, + users: interfaceConfig?.peoplePicker?.user?.users ?? defaults.peoplePicker?.user.users, + groups: interfaceConfig?.peoplePicker?.user?.groups ?? defaults.peoplePicker?.user.groups, }, }, marketplace: { admin: { - use: interfaceConfig?.marketplace?.admin?.use ?? defaults.marketplace.admin.use, + use: interfaceConfig?.marketplace?.admin?.use ?? defaults.marketplace?.admin.use, }, user: { - use: interfaceConfig?.marketplace?.user?.use ?? defaults.marketplace.user.use, + use: interfaceConfig?.marketplace?.user?.use ?? defaults.marketplace?.user.use, }, }, }); diff --git a/api/server/services/start/interface.spec.js b/api/server/services/start/interface.spec.js index 1a05c9cf12..2635a82ac1 100644 --- a/api/server/services/start/interface.spec.js +++ b/api/server/services/start/interface.spec.js @@ -27,12 +27,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -56,12 +61,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: undefined }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_GROUPS]: undefined, + [Permissions.VIEW_USERS]: undefined, + }, }); }); @@ -74,12 +84,20 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -103,12 +121,20 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -132,12 +158,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true }, + [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.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, + }, }); }); @@ -161,12 +192,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -179,12 +215,20 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined }, + [PermissionTypes.MEMORIES]: { + [Permissions.USE]: undefined, + [Permissions.OPT_OUT]: undefined, + }, [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, + }, }); }); @@ -197,12 +241,20 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -215,12 +267,20 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined }, - [PermissionTypes.MEMORIES]: { [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, + }, }); }); @@ -243,12 +303,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true }, + [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, + }, }); }); @@ -272,12 +337,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, + [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [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, + }, }); }); @@ -300,12 +370,17 @@ describe('loadDefaultInterface', () => { expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, - [PermissionTypes.MEMORIES]: { [Permissions.USE]: true }, + [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, + }, }); }); }); diff --git a/client/src/data-provider/Agents/queries.ts b/client/src/data-provider/Agents/queries.ts index 5f253f6c86..2fb81cc055 100644 --- a/client/src/data-provider/Agents/queries.ts +++ b/client/src/data-provider/Agents/queries.ts @@ -1,13 +1,12 @@ import { QueryKeys, dataService, + Permissions, EModelEndpoint, PERMISSION_BITS, PermissionTypes, - Permissions, } from 'librechat-data-provider'; import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; -import { useMemo } from 'react'; import type { QueryObserverResult, UseQueryOptions,