| | const mongoose = require('mongoose'); |
| | const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider'); |
| | const { MongoMemoryServer } = require('mongodb-memory-server'); |
| | const { fileAccess } = require('./fileAccess'); |
| | const { User, Role, AclEntry } = require('~/db/models'); |
| | const { createAgent } = require('~/models/Agent'); |
| | const { createFile } = require('~/models/File'); |
| |
|
| | describe('fileAccess middleware', () => { |
| | let mongoServer; |
| | let req, res, next; |
| | let testUser, otherUser, thirdUser; |
| |
|
| | 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, |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | testUser = await User.create({ |
| | email: 'test@example.com', |
| | name: 'Test User', |
| | username: 'testuser', |
| | role: 'test-role', |
| | }); |
| |
|
| | otherUser = await User.create({ |
| | email: 'other@example.com', |
| | name: 'Other User', |
| | username: 'otheruser', |
| | role: 'test-role', |
| | }); |
| |
|
| | thirdUser = await User.create({ |
| | email: 'third@example.com', |
| | name: 'Third User', |
| | username: 'thirduser', |
| | role: 'test-role', |
| | }); |
| |
|
| | |
| | req = { |
| | user: { id: testUser._id.toString(), role: testUser.role }, |
| | params: {}, |
| | }; |
| | res = { |
| | status: jest.fn().mockReturnThis(), |
| | json: jest.fn(), |
| | }; |
| | next = jest.fn(); |
| |
|
| | jest.clearAllMocks(); |
| | }); |
| |
|
| | describe('basic file access', () => { |
| | test('should allow access when user owns the file', async () => { |
| | |
| | await createFile({ |
| | user: testUser._id.toString(), |
| | file_id: 'file_owned_by_user', |
| | filepath: '/test/file.txt', |
| | filename: 'file.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | req.params.file_id = 'file_owned_by_user'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | expect(req.fileAccess.file).toBeDefined(); |
| | expect(res.status).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | test('should deny access when user does not own the file and no agent access', async () => { |
| | |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'file_owned_by_other', |
| | filepath: '/test/file.txt', |
| | filename: 'file.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | req.params.file_id = 'file_owned_by_other'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(403); |
| | expect(res.json).toHaveBeenCalledWith({ |
| | error: 'Forbidden', |
| | message: 'Insufficient permissions to access this file', |
| | }); |
| | }); |
| |
|
| | test('should return 404 when file does not exist', async () => { |
| | req.params.file_id = 'non_existent_file'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(404); |
| | expect(res.json).toHaveBeenCalledWith({ |
| | error: 'Not Found', |
| | message: 'File not found', |
| | }); |
| | }); |
| |
|
| | test('should return 400 when file_id is missing', async () => { |
| | |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(400); |
| | expect(res.json).toHaveBeenCalledWith({ |
| | error: 'Bad Request', |
| | message: 'file_id is required', |
| | }); |
| | }); |
| |
|
| | test('should return 401 when user is not authenticated', async () => { |
| | req.user = null; |
| | req.params.file_id = 'some_file'; |
| |
|
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(401); |
| | expect(res.json).toHaveBeenCalledWith({ |
| | error: 'Unauthorized', |
| | message: 'Authentication required', |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('agent-based file access', () => { |
| | beforeEach(async () => { |
| | |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'shared_file_via_agent', |
| | filepath: '/test/shared.txt', |
| | filename: 'shared.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| | }); |
| |
|
| | test('should allow access when user is author of agent with file', async () => { |
| | |
| | await createAgent({ |
| | id: `agent_${Date.now()}`, |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: testUser._id, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['shared_file_via_agent'], |
| | }, |
| | }, |
| | }); |
| |
|
| | req.params.file_id = 'shared_file_via_agent'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | expect(req.fileAccess.file).toBeDefined(); |
| | }); |
| |
|
| | test('should allow access when user has VIEW permission on agent with file', async () => { |
| | |
| | const agent = await createAgent({ |
| | id: `agent_${Date.now()}`, |
| | name: 'Shared Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: otherUser._id, |
| | tool_resources: { |
| | execute_code: { |
| | file_ids: ['shared_file_via_agent'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await AclEntry.create({ |
| | principalType: PrincipalType.USER, |
| | principalId: testUser._id, |
| | principalModel: PrincipalModel.USER, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | permBits: 1, |
| | grantedBy: otherUser._id, |
| | }); |
| |
|
| | req.params.file_id = 'shared_file_via_agent'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | }); |
| |
|
| | test('should check file in ocr tool_resources', async () => { |
| | await createAgent({ |
| | id: `agent_ocr_${Date.now()}`, |
| | name: 'OCR Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: testUser._id, |
| | tool_resources: { |
| | ocr: { |
| | file_ids: ['shared_file_via_agent'], |
| | }, |
| | }, |
| | }); |
| |
|
| | req.params.file_id = 'shared_file_via_agent'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | }); |
| |
|
| | test('should deny access when user has no permission on agent with file', async () => { |
| | |
| | const agent = await createAgent({ |
| | id: `agent_${Date.now()}`, |
| | name: 'Private Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: otherUser._id, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['shared_file_via_agent'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await AclEntry.create({ |
| | principalType: PrincipalType.USER, |
| | principalId: otherUser._id, |
| | principalModel: PrincipalModel.USER, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | permBits: 15, |
| | grantedBy: otherUser._id, |
| | }); |
| |
|
| | req.params.file_id = 'shared_file_via_agent'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(403); |
| | }); |
| | }); |
| |
|
| | describe('multiple agents with same file', () => { |
| | |
| | |
| | |
| | |
| | |
| |
|
| | test('should check ALL agents with file, not just first one', async () => { |
| | |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'multi_agent_file', |
| | filepath: '/test/multi.txt', |
| | filename: 'multi.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | |
| | const agent1 = await createAgent({ |
| | id: 'agent_no_access', |
| | name: 'No Access Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: otherUser._id, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['multi_agent_file'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await AclEntry.create({ |
| | principalType: PrincipalType.USER, |
| | principalId: otherUser._id, |
| | principalModel: PrincipalModel.USER, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent1._id, |
| | permBits: 15, |
| | grantedBy: otherUser._id, |
| | }); |
| |
|
| | |
| | const agent2 = await createAgent({ |
| | id: 'agent_with_access', |
| | name: 'Accessible Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: thirdUser._id, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['multi_agent_file'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await AclEntry.create({ |
| | principalType: PrincipalType.USER, |
| | principalId: testUser._id, |
| | principalModel: PrincipalModel.USER, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent2._id, |
| | permBits: 1, |
| | grantedBy: thirdUser._id, |
| | }); |
| |
|
| | req.params.file_id = 'multi_agent_file'; |
| | await fileAccess(req, res, next); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | expect(res.status).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | test('should find file in any agent tool_resources type', async () => { |
| | |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'multi_tool_file', |
| | filepath: '/test/tool.txt', |
| | filename: 'tool.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | |
| | await createAgent({ |
| | id: 'agent_file_search', |
| | name: 'File Search Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: otherUser._id, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['multi_tool_file'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await createAgent({ |
| | id: 'agent_execute_code', |
| | name: 'Execute Code Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: thirdUser._id, |
| | tool_resources: { |
| | execute_code: { |
| | file_ids: ['multi_tool_file'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | await createAgent({ |
| | id: 'agent_ocr', |
| | name: 'OCR Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: testUser._id, |
| | tool_resources: { |
| | ocr: { |
| | file_ids: ['multi_tool_file'], |
| | }, |
| | }, |
| | }); |
| |
|
| | req.params.file_id = 'multi_tool_file'; |
| | await fileAccess(req, res, next); |
| |
|
| | |
| | |
| | |
| | |
| | expect(next).toHaveBeenCalled(); |
| | expect(req.fileAccess).toBeDefined(); |
| | }); |
| | }); |
| |
|
| | describe('edge cases', () => { |
| | test('should handle agent with empty tool_resources', async () => { |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'orphan_file', |
| | filepath: '/test/orphan.txt', |
| | filename: 'orphan.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | |
| | await createAgent({ |
| | id: `agent_empty_${Date.now()}`, |
| | name: 'Empty Resources Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: testUser._id, |
| | tool_resources: {}, |
| | }); |
| |
|
| | req.params.file_id = 'orphan_file'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(403); |
| | }); |
| |
|
| | test('should handle agent with null tool_resources', async () => { |
| | await createFile({ |
| | user: otherUser._id.toString(), |
| | file_id: 'another_orphan_file', |
| | filepath: '/test/orphan2.txt', |
| | filename: 'orphan2.txt', |
| | type: 'text/plain', |
| | size: 100, |
| | }); |
| |
|
| | |
| | await createAgent({ |
| | id: `agent_null_${Date.now()}`, |
| | name: 'Null Resources Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: testUser._id, |
| | tool_resources: null, |
| | }); |
| |
|
| | req.params.file_id = 'another_orphan_file'; |
| | await fileAccess(req, res, next); |
| |
|
| | expect(next).not.toHaveBeenCalled(); |
| | expect(res.status).toHaveBeenCalledWith(403); |
| | }); |
| | }); |
| | }); |
| |
|