| const mongoose = require('mongoose'); |
| const { v4: uuidv4 } = require('uuid'); |
| const { createModels } = require('@librechat/data-schemas'); |
| const { MongoMemoryServer } = require('mongodb-memory-server'); |
| const { |
| SystemRoles, |
| ResourceType, |
| AccessRoleIds, |
| PrincipalType, |
| } = require('librechat-data-provider'); |
| const { grantPermission } = require('~/server/services/PermissionService'); |
| const { getFiles, createFile } = require('./File'); |
| const { seedDefaultRoles } = require('~/models'); |
| const { createAgent } = require('./Agent'); |
|
|
| let File; |
| let Agent; |
| let AclEntry; |
| let User; |
| let modelsToCleanup = []; |
|
|
| describe('File Access Control', () => { |
| let mongoServer; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| await mongoose.connect(mongoUri); |
|
|
| |
| const models = createModels(mongoose); |
|
|
| |
| modelsToCleanup = Object.keys(models); |
|
|
| |
| const dbModels = require('~/db/models'); |
| Object.assign(mongoose.models, dbModels); |
|
|
| File = dbModels.File; |
| Agent = dbModels.Agent; |
| AclEntry = dbModels.AclEntry; |
| User = dbModels.User; |
|
|
| |
| await seedDefaultRoles(); |
| }); |
|
|
| afterAll(async () => { |
| |
| const collections = mongoose.connection.collections; |
| for (const key in collections) { |
| await collections[key].deleteMany({}); |
| } |
|
|
| |
| for (const modelName of modelsToCleanup) { |
| if (mongoose.models[modelName]) { |
| delete mongoose.models[modelName]; |
| } |
| } |
|
|
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await File.deleteMany({}); |
| await Agent.deleteMany({}); |
| await AclEntry.deleteMany({}); |
| await User.deleteMany({}); |
| |
| }); |
|
|
| describe('hasAccessToFilesViaAgent', () => { |
| it('should efficiently check access for multiple files at once', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileIds = [uuidv4(), uuidv4(), uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| for (const fileId of fileIds) { |
| await createFile({ |
| user: authorId, |
| file_id: fileId, |
| filename: `file-${fileId}.txt`, |
| filepath: `/uploads/${fileId}`, |
| }); |
| } |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'Test Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: [fileIds[0], fileIds[1]], |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: userId, |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_EDITOR, |
| grantedBy: authorId, |
| }); |
|
|
| |
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMap = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: SystemRoles.USER, |
| fileIds, |
| agentId: agent.id, |
| }); |
|
|
| |
| expect(accessMap.get(fileIds[0])).toBe(true); |
| expect(accessMap.get(fileIds[1])).toBe(true); |
| expect(accessMap.get(fileIds[2])).toBe(false); |
| expect(accessMap.get(fileIds[3])).toBe(false); |
| }); |
|
|
| it('should grant access to all files when user is the agent author', async () => { |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileIds = [uuidv4(), uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| await createAgent({ |
| id: agentId, |
| name: 'Test Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: [fileIds[0]], |
| }, |
| }, |
| }); |
|
|
| |
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMap = await hasAccessToFilesViaAgent({ |
| userId: authorId, |
| role: SystemRoles.USER, |
| fileIds, |
| agentId, |
| }); |
|
|
| |
| expect(accessMap.get(fileIds[0])).toBe(true); |
| expect(accessMap.get(fileIds[1])).toBe(true); |
| expect(accessMap.get(fileIds[2])).toBe(true); |
| }); |
|
|
| it('should handle non-existent agent gracefully', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const fileIds = [uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMap = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: SystemRoles.USER, |
| fileIds, |
| agentId: 'non-existent-agent', |
| }); |
|
|
| |
| expect(accessMap.get(fileIds[0])).toBe(false); |
| expect(accessMap.get(fileIds[1])).toBe(false); |
| }); |
|
|
| it('should deny access when user only has VIEW permission and needs access for deletion', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileIds = [uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'View-Only Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: fileIds, |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: userId, |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_VIEWER, |
| grantedBy: authorId, |
| }); |
|
|
| |
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMap = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: SystemRoles.USER, |
| fileIds, |
| agentId, |
| isDelete: true, |
| }); |
|
|
| |
| expect(accessMap.get(fileIds[0])).toBe(false); |
| expect(accessMap.get(fileIds[1])).toBe(false); |
| }); |
|
|
| it('should grant access when user has VIEW permission', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileIds = [uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'View-Only Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: fileIds, |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: userId, |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_VIEWER, |
| grantedBy: authorId, |
| }); |
|
|
| |
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMap = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: SystemRoles.USER, |
| fileIds, |
| agentId, |
| }); |
|
|
| expect(accessMap.get(fileIds[0])).toBe(true); |
| expect(accessMap.get(fileIds[1])).toBe(true); |
| }); |
| }); |
|
|
| describe('getFiles with agent access control', () => { |
| test('should return files owned by user and files accessible through agent', async () => { |
| const authorId = new mongoose.Types.ObjectId(); |
| const userId = new mongoose.Types.ObjectId(); |
| const agentId = `agent_${uuidv4()}`; |
| const ownedFileId = `file_${uuidv4()}`; |
| const sharedFileId = `file_${uuidv4()}`; |
| const inaccessibleFileId = `file_${uuidv4()}`; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'Shared Agent', |
| provider: 'test', |
| model: 'test-model', |
| author: authorId, |
| tool_resources: { |
| file_search: { |
| file_ids: [sharedFileId], |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: userId, |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_EDITOR, |
| grantedBy: authorId, |
| }); |
|
|
| |
| await createFile({ |
| file_id: ownedFileId, |
| user: userId, |
| filename: 'owned.txt', |
| filepath: '/uploads/owned.txt', |
| type: 'text/plain', |
| bytes: 100, |
| }); |
|
|
| await createFile({ |
| file_id: sharedFileId, |
| user: authorId, |
| filename: 'shared.txt', |
| filepath: '/uploads/shared.txt', |
| type: 'text/plain', |
| bytes: 200, |
| embedded: true, |
| }); |
|
|
| await createFile({ |
| file_id: inaccessibleFileId, |
| user: authorId, |
| filename: 'inaccessible.txt', |
| filepath: '/uploads/inaccessible.txt', |
| type: 'text/plain', |
| bytes: 300, |
| }); |
|
|
| |
| const allFiles = await getFiles( |
| { file_id: { $in: [ownedFileId, sharedFileId, inaccessibleFileId] } }, |
| null, |
| { text: 0 }, |
| ); |
|
|
| |
| const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); |
| const files = await filterFilesByAgentAccess({ |
| files: allFiles, |
| userId: userId, |
| role: SystemRoles.USER, |
| agentId, |
| }); |
|
|
| expect(files).toHaveLength(2); |
| expect(files.map((f) => f.file_id)).toContain(ownedFileId); |
| expect(files.map((f) => f.file_id)).toContain(sharedFileId); |
| expect(files.map((f) => f.file_id)).not.toContain(inaccessibleFileId); |
| }); |
|
|
| test('should return all files when no userId/agentId provided', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const fileId1 = `file_${uuidv4()}`; |
| const fileId2 = `file_${uuidv4()}`; |
|
|
| await createFile({ |
| file_id: fileId1, |
| user: userId, |
| filename: 'file1.txt', |
| filepath: '/uploads/file1.txt', |
| type: 'text/plain', |
| bytes: 100, |
| }); |
|
|
| await createFile({ |
| file_id: fileId2, |
| user: new mongoose.Types.ObjectId(), |
| filename: 'file2.txt', |
| filepath: '/uploads/file2.txt', |
| type: 'text/plain', |
| bytes: 200, |
| }); |
|
|
| const files = await getFiles({ file_id: { $in: [fileId1, fileId2] } }); |
| expect(files).toHaveLength(2); |
| }); |
| }); |
|
|
| describe('Role-based file permissions', () => { |
| it('should optimize permission checks when role is provided', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileIds = [uuidv4(), uuidv4()]; |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| role: 'ADMIN', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| for (const fileId of fileIds) { |
| await createFile({ |
| file_id: fileId, |
| user: authorId, |
| filename: `${fileId}.txt`, |
| filepath: `/uploads/${fileId}.txt`, |
| type: 'text/plain', |
| bytes: 100, |
| }); |
| } |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'Test Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: fileIds, |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.ROLE, |
| principalId: 'ADMIN', |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_EDITOR, |
| grantedBy: authorId, |
| }); |
|
|
| |
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
| const accessMapWithRole = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: 'ADMIN', |
| fileIds, |
| agentId: agent.id, |
| }); |
|
|
| |
| expect(accessMapWithRole.get(fileIds[0])).toBe(true); |
| expect(accessMapWithRole.get(fileIds[1])).toBe(true); |
|
|
| |
| const accessMapWithoutRole = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| fileIds, |
| agentId: agent.id, |
| }); |
|
|
| |
| expect(accessMapWithoutRole.get(fileIds[0])).toBe(true); |
| expect(accessMapWithoutRole.get(fileIds[1])).toBe(true); |
| }); |
|
|
| it('should deny access when user role changes', async () => { |
| const userId = new mongoose.Types.ObjectId(); |
| const authorId = new mongoose.Types.ObjectId(); |
| const agentId = uuidv4(); |
| const fileId = uuidv4(); |
|
|
| |
| await User.create({ |
| _id: userId, |
| email: 'user@example.com', |
| emailVerified: true, |
| provider: 'local', |
| role: 'EDITOR', |
| }); |
|
|
| await User.create({ |
| _id: authorId, |
| email: 'author@example.com', |
| emailVerified: true, |
| provider: 'local', |
| }); |
|
|
| |
| await createFile({ |
| file_id: fileId, |
| user: authorId, |
| filename: 'test.txt', |
| filepath: '/uploads/test.txt', |
| type: 'text/plain', |
| bytes: 100, |
| }); |
|
|
| |
| const agent = await createAgent({ |
| id: agentId, |
| name: 'Test Agent', |
| author: authorId, |
| model: 'gpt-4', |
| provider: 'openai', |
| tool_resources: { |
| file_search: { |
| file_ids: [fileId], |
| }, |
| }, |
| }); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.ROLE, |
| principalId: 'EDITOR', |
| resourceType: ResourceType.AGENT, |
| resourceId: agent._id, |
| accessRoleId: AccessRoleIds.AGENT_EDITOR, |
| grantedBy: authorId, |
| }); |
|
|
| const { hasAccessToFilesViaAgent } = require('~/server/services/Files/permissions'); |
|
|
| |
| const accessAsEditor = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: 'EDITOR', |
| fileIds: [fileId], |
| agentId: agent.id, |
| }); |
| expect(accessAsEditor.get(fileId)).toBe(true); |
|
|
| |
| const accessAsUser = await hasAccessToFilesViaAgent({ |
| userId: userId, |
| role: SystemRoles.USER, |
| fileIds: [fileId], |
| agentId: agent.id, |
| }); |
| expect(accessAsUser.get(fileId)).toBe(false); |
| }); |
| }); |
| }); |
|
|