import { Request, Response, NextFunction } from 'express'; import { Permissions, PermissionTypes, EModelEndpoint, EndpointURLs, } from 'librechat-data-provider'; import type { IRole, IUser } from '@librechat/data-schemas'; import { checkAccess, generateCheckAccess, skipAgentCheck } from './access'; // Mock logger jest.mock('@librechat/data-schemas', () => ({ logger: { warn: jest.fn(), error: jest.fn(), debug: jest.fn(), }, })); describe('access middleware', () => { let mockReq: Partial; let mockRes: Partial; let mockNext: jest.MockedFunction; let mockGetRoleByName: jest.Mock; beforeEach(() => { mockReq = { user: { id: 'user123', role: 'user', email: 'test@example.com', emailVerified: true, provider: 'local', } as IUser, body: {}, originalUrl: '/api/test', method: 'POST', } as Partial; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; mockNext = jest.fn() as jest.MockedFunction; mockGetRoleByName = jest.fn(); }); afterEach(() => { jest.clearAllMocks(); }); describe('skipAgentCheck', () => { it('should return false when req is undefined', () => { expect(skipAgentCheck(undefined)).toBe(false); }); it('should return false when req.body.endpoint is not present', () => { expect(skipAgentCheck(mockReq as Request)).toBe(false); }); it('should return false when method is not POST', () => { mockReq.method = 'GET'; mockReq.body = { endpoint: 'gpt-4' }; expect(skipAgentCheck(mockReq as Request)).toBe(false); }); it('should return false when URL does not include agents endpoint', () => { mockReq.body = { endpoint: 'gpt-4' }; mockReq.originalUrl = '/api/messages'; expect(skipAgentCheck(mockReq as Request)).toBe(false); }); it('should return true when not an agents endpoint but URL includes agents', () => { mockReq.body = { endpoint: 'gpt-4' }; mockReq.originalUrl = EndpointURLs[EModelEndpoint.agents]; expect(skipAgentCheck(mockReq as Request)).toBe(true); }); it('should return false when is an agents endpoint', () => { mockReq.body = { endpoint: EModelEndpoint.agents }; mockReq.originalUrl = EndpointURLs[EModelEndpoint.agents]; expect(skipAgentCheck(mockReq as Request)).toBe(false); }); }); describe('checkAccess', () => { const defaultParams = { user: { id: 'user123', role: 'user', email: 'test@example.com', emailVerified: true, provider: 'local', } as IUser, permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE], getRoleByName: jest.fn(), }; it('should return true when skipCheck function returns true', async () => { const skipCheck = jest.fn().mockReturnValue(true); const result = await checkAccess({ ...defaultParams, req: mockReq as Request, skipCheck, }); expect(result).toBe(true); expect(skipCheck).toHaveBeenCalledWith(mockReq); }); it('should return false when user is not provided', async () => { const result = await checkAccess({ ...defaultParams, user: null as unknown as IUser, }); expect(result).toBe(false); }); it('should return false when user has no role', async () => { const result = await checkAccess({ ...defaultParams, user: { id: 'user123', email: 'test@example.com', emailVerified: true, provider: 'local', } as IUser, }); expect(result).toBe(false); }); it('should return true when user has required permissions', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const result = await checkAccess(defaultParams); expect(result).toBe(true); expect(defaultParams.getRoleByName).toHaveBeenCalledWith('user'); }); it('should return false when user lacks required permissions', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: false, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const result = await checkAccess(defaultParams); expect(result).toBe(false); }); it('should check multiple permissions with AND logic', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const result = await checkAccess({ ...defaultParams, permissions: [Permissions.USE, Permissions.CREATE], }); expect(result).toBe(true); }); it('should return false when user has only some of the required permissions', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: false, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const result = await checkAccess({ ...defaultParams, permissions: [Permissions.USE, Permissions.CREATE], }); expect(result).toBe(false); }); it('should check bodyProps when permission is not directly granted', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.SHARED_GLOBAL]: false, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const checkObject = { projectIds: ['project1'], removeProjectIds: ['project2'], }; const result = await checkAccess({ ...defaultParams, permissions: [Permissions.USE, Permissions.SHARED_GLOBAL], bodyProps: { [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], } as Record, checkObject, }); expect(result).toBe(true); }); it('should return false when bodyProps requirements are not met', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.SHARED_GLOBAL]: false, }, }, } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const checkObject = { projectIds: ['project1'], // missing removeProjectIds }; const result = await checkAccess({ ...defaultParams, permissions: [Permissions.SHARED_GLOBAL], bodyProps: { [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], } as Record, checkObject, }); expect(result).toBe(false); }); it('should handle role without permissions object', async () => { const mockRole = { name: 'user', } as unknown as IRole; defaultParams.getRoleByName.mockResolvedValue(mockRole); const result = await checkAccess(defaultParams); expect(result).toBe(false); }); it('should handle getRoleByName returning null', async () => { defaultParams.getRoleByName.mockResolvedValue(null); const result = await checkAccess(defaultParams); expect(result).toBe(false); }); }); describe('generateCheckAccess', () => { it('should create middleware that allows access when user has permissions', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.READ]: true, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); const middleware = generateCheckAccess({ permissionType: PermissionTypes.MEMORIES, permissions: [Permissions.USE, Permissions.READ], getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockRes.status).not.toHaveBeenCalled(); }); it('should create middleware that denies access when user lacks permissions', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.MEMORIES]: { [Permissions.USE]: false, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); const middleware = generateCheckAccess({ permissionType: PermissionTypes.MEMORIES, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).not.toHaveBeenCalled(); expect(mockRes.status).toHaveBeenCalledWith(403); expect(mockRes.json).toHaveBeenCalledWith({ message: 'Forbidden: Insufficient permissions' }); }); it('should handle bodyProps in middleware', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARED_GLOBAL]: false, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); mockReq.body = { projectIds: ['project1'], removeProjectIds: ['project2'], }; const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARED_GLOBAL], bodyProps: { [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], } as Record, getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockRes.status).not.toHaveBeenCalled(); }); it('should use skipCheck function when provided', async () => { const skipCheck = jest.fn().mockReturnValue(true); const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE], skipCheck, getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(skipCheck).toHaveBeenCalledWith(mockReq); expect(mockNext).toHaveBeenCalled(); expect(mockGetRoleByName).not.toHaveBeenCalled(); }); it('should handle errors and return 500 status', async () => { const error = new Error('Database error'); mockGetRoleByName.mockRejectedValue(error); const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith({ message: 'Server error: Database error', }); }); it('should handle non-Error objects in catch block', async () => { mockGetRoleByName.mockRejectedValue('String error'); const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); await middleware(mockReq as Request, mockRes as Response, mockNext); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith({ message: 'Server error: Unknown error', }); }); }); describe('Real-world usage patterns', () => { it('should handle memory access patterns', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.UPDATE]: true, [Permissions.READ]: true, [Permissions.OPT_OUT]: true, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); // Test memory read access const checkMemoryRead = generateCheckAccess({ permissionType: PermissionTypes.MEMORIES, permissions: [Permissions.USE, Permissions.READ], getRoleByName: mockGetRoleByName, }); await checkMemoryRead(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); // Test memory create access mockNext.mockClear(); const checkMemoryCreate = generateCheckAccess({ permissionType: PermissionTypes.MEMORIES, permissions: [Permissions.USE, Permissions.CREATE], getRoleByName: mockGetRoleByName, }); await checkMemoryCreate(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); }); it('should handle agent access patterns with skipCheck', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); mockReq.body = { endpoint: 'gpt-4' }; mockReq.originalUrl = EndpointURLs[EModelEndpoint.agents]; const checkAgentAccess = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE], skipCheck: skipAgentCheck, getRoleByName: mockGetRoleByName, }); await checkAgentAccess(mockReq as Request, mockRes as Response, mockNext); // Should skip check because endpoint is not agents expect(mockNext).toHaveBeenCalled(); expect(mockGetRoleByName).not.toHaveBeenCalled(); }); it('should handle prompt access patterns', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARED_GLOBAL]: false, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); const checkPromptAccess = generateCheckAccess({ permissionType: PermissionTypes.PROMPTS, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); await checkPromptAccess(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); }); it('should handle bookmark access patterns', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); const checkBookmarkAccess = generateCheckAccess({ permissionType: PermissionTypes.BOOKMARKS, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); await checkBookmarkAccess(mockReq as Request, mockRes as Response, mockNext); expect(mockNext).toHaveBeenCalled(); }); it('should handle tool access patterns', async () => { const mockRole = { name: 'user', permissions: { [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true, }, }, } as unknown as IRole; mockGetRoleByName.mockResolvedValue(mockRole); const result = await checkAccess({ user: mockReq.user as IUser, permissionType: PermissionTypes.RUN_CODE, permissions: [Permissions.USE], getRoleByName: mockGetRoleByName, }); expect(result).toBe(true); }); }); });