| | 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'; |
| |
|
| | |
| | jest.mock('@librechat/data-schemas', () => ({ |
| | logger: { |
| | warn: jest.fn(), |
| | error: jest.fn(), |
| | debug: jest.fn(), |
| | }, |
| | })); |
| |
|
| | describe('access middleware', () => { |
| | let mockReq: Partial<Request>; |
| | let mockRes: Partial<Response>; |
| | let mockNext: jest.MockedFunction<NextFunction>; |
| | 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<Request>; |
| |
|
| | mockRes = { |
| | status: jest.fn().mockReturnThis(), |
| | json: jest.fn().mockReturnThis(), |
| | }; |
| |
|
| | mockNext = jest.fn() as jest.MockedFunction<NextFunction>; |
| | 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<Permissions, string[]>, |
| | 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'], |
| | |
| | }; |
| |
|
| | const result = await checkAccess({ |
| | ...defaultParams, |
| | permissions: [Permissions.SHARED_GLOBAL], |
| | bodyProps: { |
| | [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], |
| | } as Record<Permissions, string[]>, |
| | 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<Permissions, string[]>, |
| | 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); |
| |
|
| | |
| | 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(); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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); |
| | }); |
| | }); |
| | }); |
| |
|