From f89742058626561705428e4679f7308fe22ee757 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 9 Jun 2025 17:50:11 -0400 Subject: [PATCH] ci: add unit tests for access control middleware - Introduced tests for the `canAccessAgentResource` middleware to validate permission checks for agent resources. - Implemented tests for various scenarios including user roles, ACL entries, and permission levels. - Added tests for the `checkAccess` function to ensure proper permission handling based on user roles and permissions. - Utilized MongoDB in-memory server for isolated test environments. --- .../canAccessAgentResource.spec.js | 384 ++++++++++++++++++ api/server/middleware/roles/access.spec.js | 251 ++++++++++++ 2 files changed, 635 insertions(+) create mode 100644 api/server/middleware/accessResources/canAccessAgentResource.spec.js create mode 100644 api/server/middleware/roles/access.spec.js diff --git a/api/server/middleware/accessResources/canAccessAgentResource.spec.js b/api/server/middleware/accessResources/canAccessAgentResource.spec.js new file mode 100644 index 0000000000..4e4a0b7de0 --- /dev/null +++ b/api/server/middleware/accessResources/canAccessAgentResource.spec.js @@ -0,0 +1,384 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { canAccessAgentResource } = require('./canAccessAgentResource'); +const { User, Role, AclEntry } = require('~/db/models'); +const { createAgent } = require('~/models/Agent'); + +describe('canAccessAgentResource middleware', () => { + let mongoServer; + let req, res, next; + let testUser; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await mongoose.connection.dropDatabase(); + await Role.create({ + name: 'test-role', + permissions: { + AGENTS: { + USE: true, + CREATE: true, + SHARED_GLOBAL: false, + }, + }, + }); + + // Create a test user + testUser = await User.create({ + email: 'test@example.com', + name: 'Test User', + username: 'testuser', + role: 'test-role', + }); + + req = { + user: { id: testUser._id.toString(), role: 'test-role' }, + params: {}, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + next = jest.fn(); + + jest.clearAllMocks(); + }); + + describe('middleware factory', () => { + test('should throw error if requiredPermission is not provided', () => { + expect(() => canAccessAgentResource({})).toThrow( + 'canAccessAgentResource: requiredPermission is required and must be a number', + ); + }); + + test('should throw error if requiredPermission is not a number', () => { + expect(() => canAccessAgentResource({ requiredPermission: '1' })).toThrow( + 'canAccessAgentResource: requiredPermission is required and must be a number', + ); + }); + + test('should create middleware with default resourceIdParam', () => { + const middleware = canAccessAgentResource({ requiredPermission: 1 }); + expect(typeof middleware).toBe('function'); + expect(middleware.length).toBe(3); // Express middleware signature + }); + + test('should create middleware with custom resourceIdParam', () => { + const middleware = canAccessAgentResource({ + requiredPermission: 2, + resourceIdParam: 'agent_id', + }); + expect(typeof middleware).toBe('function'); + expect(middleware.length).toBe(3); + }); + }); + + describe('permission checking with real agents', () => { + test('should allow access when user is the agent author', async () => { + // Create an agent owned by the test user + const agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: testUser._id, + }); + + // Create ACL entry for the author (owner permissions) + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 15, // All permissions (1+2+4+8) + grantedBy: testUser._id, + }); + + req.params.id = agent.id; + + const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('should deny access when user is not the author and has no ACL entry', async () => { + // Create an agent owned by a different user + const otherUser = await User.create({ + email: 'other@example.com', + name: 'Other User', + username: 'otheruser', + role: 'test-role', + }); + + const agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Other User Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + // Create ACL entry for the other user (owner) + await AclEntry.create({ + principalType: 'user', + principalId: otherUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 15, // All permissions + grantedBy: otherUser._id, + }); + + req.params.id = agent.id; + + const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to access this agent', + }); + }); + + test('should allow access when user has ACL entry with sufficient permissions', async () => { + // Create an agent owned by a different user + const otherUser = await User.create({ + email: 'other2@example.com', + name: 'Other User 2', + username: 'otheruser2', + role: 'test-role', + }); + + const agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Shared Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + // Create ACL entry granting view permission to test user + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 1, // VIEW permission + grantedBy: otherUser._id, + }); + + req.params.id = agent.id; + + const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('should deny access when ACL permissions are insufficient', async () => { + // Create an agent owned by a different user + const otherUser = await User.create({ + email: 'other3@example.com', + name: 'Other User 3', + username: 'otheruser3', + role: 'test-role', + }); + + const agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Limited Access Agent', + provider: 'openai', + model: 'gpt-4', + author: otherUser._id, + }); + + // Create ACL entry granting only view permission + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 1, // VIEW permission only + grantedBy: otherUser._id, + }); + + req.params.id = agent.id; + + const middleware = canAccessAgentResource({ requiredPermission: 2 }); // EDIT permission required + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: 'Insufficient permissions to access this agent', + }); + }); + + test('should handle non-existent agent', async () => { + req.params.id = 'agent_nonexistent'; + + const middleware = canAccessAgentResource({ requiredPermission: 1 }); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: 'Not Found', + message: 'agent not found', + }); + }); + + test('should use custom resourceIdParam', async () => { + const agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Custom Param Agent', + provider: 'openai', + model: 'gpt-4', + author: testUser._id, + }); + + // Create ACL entry for the author + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 15, // All permissions + grantedBy: testUser._id, + }); + + req.params.agent_id = agent.id; // Using custom param name + + const middleware = canAccessAgentResource({ + requiredPermission: 1, + resourceIdParam: 'agent_id', + }); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + }); + + describe('permission levels', () => { + let agent; + + beforeEach(async () => { + agent = await createAgent({ + id: `agent_${Date.now()}`, + name: 'Permission Test Agent', + provider: 'openai', + model: 'gpt-4', + author: testUser._id, + }); + + // Create ACL entry with all permissions for the owner + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 15, // All permissions (1+2+4+8) + grantedBy: testUser._id, + }); + + req.params.id = agent.id; + }); + + test('should support view permission (1)', async () => { + const middleware = canAccessAgentResource({ requiredPermission: 1 }); + await middleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + + test('should support edit permission (2)', async () => { + const middleware = canAccessAgentResource({ requiredPermission: 2 }); + await middleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + + test('should support delete permission (4)', async () => { + const middleware = canAccessAgentResource({ requiredPermission: 4 }); + await middleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + + test('should support share permission (8)', async () => { + const middleware = canAccessAgentResource({ requiredPermission: 8 }); + await middleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + + test('should support combined permissions', async () => { + const viewAndEdit = 1 | 2; // 3 + const middleware = canAccessAgentResource({ requiredPermission: viewAndEdit }); + await middleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + }); + + describe('integration with agent operations', () => { + test('should work with agent CRUD operations', async () => { + const agentId = `agent_${Date.now()}`; + + // Create agent + const agent = await createAgent({ + id: agentId, + name: 'Integration Test Agent', + provider: 'openai', + model: 'gpt-4', + author: testUser._id, + description: 'Testing integration', + }); + + // Create ACL entry for the author + await AclEntry.create({ + principalType: 'user', + principalId: testUser._id, + principalModel: 'User', + resourceType: 'agent', + resourceId: agent._id, + permBits: 15, // All permissions + grantedBy: testUser._id, + }); + + req.params.id = agentId; + + // Test view access + const viewMiddleware = canAccessAgentResource({ requiredPermission: 1 }); + await viewMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); + jest.clearAllMocks(); + + // Update the agent + const { updateAgent } = require('~/models/Agent'); + await updateAgent({ id: agentId }, { description: 'Updated description' }); + + // Test edit access + const editMiddleware = canAccessAgentResource({ requiredPermission: 2 }); + await editMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); + }); + }); +}); diff --git a/api/server/middleware/roles/access.spec.js b/api/server/middleware/roles/access.spec.js new file mode 100644 index 0000000000..435b20e9fb --- /dev/null +++ b/api/server/middleware/roles/access.spec.js @@ -0,0 +1,251 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { checkAccess, generateCheckAccess } = require('./access'); +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { Role } = require('~/db/models'); + +// Mock only the logger +jest.mock('~/config', () => ({ + logger: { + warn: jest.fn(), + error: jest.fn(), + }, +})); + +describe('Access Middleware', () => { + let mongoServer; + let req, res, next; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await mongoose.connection.dropDatabase(); + + // Create test roles + await Role.create({ + name: 'user', + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: false, + [Permissions.SHARED_GLOBAL]: false, + }, + }, + }); + + await Role.create({ + name: 'admin', + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARED_GLOBAL]: true, + }, + }, + }); + + req = { + user: { id: 'user123', role: 'user' }, + body: {}, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + next = jest.fn(); + jest.clearAllMocks(); + }); + + describe('checkAccess', () => { + test('should return false if user is not provided', async () => { + const result = await checkAccess(null, PermissionTypes.AGENTS, [Permissions.USE]); + expect(result).toBe(false); + }); + + test('should return true if user has required permission', async () => { + const result = await checkAccess(req.user, PermissionTypes.AGENTS, [Permissions.USE]); + expect(result).toBe(true); + }); + + test('should return false if user lacks required permission', async () => { + const result = await checkAccess(req.user, PermissionTypes.AGENTS, [Permissions.CREATE]); + expect(result).toBe(false); + }); + + test('should return true if user has any of multiple permissions', async () => { + const result = await checkAccess(req.user, PermissionTypes.AGENTS, [ + Permissions.USE, + Permissions.CREATE, + ]); + expect(result).toBe(true); + }); + + test('should check body properties when permission is not directly granted', async () => { + // User role doesn't have CREATE permission, but bodyProps allows it + const bodyProps = { + [Permissions.CREATE]: ['agentId', 'name'], + }; + + const checkObject = { agentId: 'agent123' }; + + const result = await checkAccess( + req.user, + PermissionTypes.AGENTS, + [Permissions.CREATE], + bodyProps, + checkObject, + ); + expect(result).toBe(true); + }); + + test('should return false if role is not found', async () => { + req.user.role = 'nonexistent'; + const result = await checkAccess(req.user, PermissionTypes.AGENTS, [Permissions.USE]); + expect(result).toBe(false); + }); + + test('should return false if role has no permissions for the requested type', async () => { + await Role.create({ + name: 'limited', + permissions: { + // Explicitly set AGENTS permissions to false + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARED_GLOBAL]: false, + }, + // Has permissions for other types + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + }, + }, + }); + req.user.role = 'limited'; + + const result = await checkAccess(req.user, PermissionTypes.AGENTS, [Permissions.USE]); + expect(result).toBe(false); + }); + + test('should handle admin role with all permissions', async () => { + req.user.role = 'admin'; + + const createResult = await checkAccess(req.user, PermissionTypes.AGENTS, [ + Permissions.CREATE, + ]); + expect(createResult).toBe(true); + + const shareResult = await checkAccess(req.user, PermissionTypes.AGENTS, [ + Permissions.SHARED_GLOBAL, + ]); + expect(shareResult).toBe(true); + }); + }); + + describe('generateCheckAccess', () => { + test('should call next() when user has required permission', async () => { + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('should return 403 when user lacks permission', async () => { + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.CREATE]); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ message: 'Forbidden: Insufficient permissions' }); + }); + + test('should check body properties when configured', async () => { + req.body = { agentId: 'agent123', description: 'test' }; + + const bodyProps = { + [Permissions.CREATE]: ['agentId'], + }; + + const middleware = generateCheckAccess( + PermissionTypes.AGENTS, + [Permissions.CREATE], + bodyProps, + ); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + test('should handle database errors gracefully', async () => { + // Create a user with an invalid role that will cause getRoleByName to fail + req.user.role = { invalid: 'object' }; // This will cause an error when querying + + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + message: expect.stringContaining('Server error:'), + }); + }); + + test('should work with multiple permission types', async () => { + req.user.role = 'admin'; + + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [ + Permissions.USE, + Permissions.CREATE, + Permissions.SHARED_GLOBAL, + ]); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('should handle missing user gracefully', async () => { + req.user = null; + + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + message: expect.stringContaining('Server error:'), + }); + }); + + test('should handle role with no AGENTS permissions', async () => { + await Role.create({ + name: 'noaccess', + permissions: { + // Explicitly set AGENTS with all permissions false + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARED_GLOBAL]: false, + }, + }, + }); + req.user.role = 'noaccess'; + + const middleware = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]); + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ message: 'Forbidden: Insufficient permissions' }); + }); + }); +});