| | 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); |
| | }); |
| | }); |
| | }); |
| |
|