mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00

* 🔧 fix: Add localized message for delete operation not allowed
* refactor: improve file deletion operations ux
* feat: agent-based file access control and enhance file retrieval logic
* feat: implement agent-specific file retrieval
* feat: enhance agent file retrieval logic for authors and shared access
* ci: include userId and agentId in mockGetFiles call for OCR file retrieval
264 lines
8.4 KiB
JavaScript
264 lines
8.4 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { fileSchema } = require('@librechat/data-schemas');
|
|
const { agentSchema } = require('@librechat/data-schemas');
|
|
const { projectSchema } = require('@librechat/data-schemas');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
|
const { getFiles, createFile } = require('./File');
|
|
const { getProjectByName } = require('./Project');
|
|
const { createAgent } = require('./Agent');
|
|
|
|
let File;
|
|
let Agent;
|
|
let Project;
|
|
|
|
describe('File Access Control', () => {
|
|
let mongoServer;
|
|
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
File = mongoose.models.File || mongoose.model('File', fileSchema);
|
|
Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema);
|
|
Project = mongoose.models.Project || mongoose.model('Project', projectSchema);
|
|
await mongoose.connect(mongoUri);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await File.deleteMany({});
|
|
await Agent.deleteMany({});
|
|
await Project.deleteMany({});
|
|
});
|
|
|
|
describe('hasAccessToFilesViaAgent', () => {
|
|
it('should efficiently check access for multiple files at once', async () => {
|
|
const userId = new mongoose.Types.ObjectId().toString();
|
|
const authorId = new mongoose.Types.ObjectId().toString();
|
|
const agentId = uuidv4();
|
|
const fileIds = [uuidv4(), uuidv4(), uuidv4(), uuidv4()];
|
|
|
|
// Create files
|
|
for (const fileId of fileIds) {
|
|
await createFile({
|
|
user: authorId,
|
|
file_id: fileId,
|
|
filename: `file-${fileId}.txt`,
|
|
filepath: `/uploads/${fileId}`,
|
|
});
|
|
}
|
|
|
|
// Create agent with only first two files attached
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Test Agent',
|
|
author: authorId,
|
|
model: 'gpt-4',
|
|
provider: 'openai',
|
|
isCollaborative: true,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileIds[0], fileIds[1]],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Get or create global project
|
|
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, '_id');
|
|
|
|
// Share agent globally
|
|
await Agent.updateOne({ id: agentId }, { $push: { projectIds: globalProject._id } });
|
|
|
|
// Check access for all files
|
|
const { hasAccessToFilesViaAgent } = require('./File');
|
|
const accessMap = await hasAccessToFilesViaAgent(userId, fileIds, agentId);
|
|
|
|
// Should have access only to the first two files
|
|
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().toString();
|
|
const agentId = uuidv4();
|
|
const fileIds = [uuidv4(), uuidv4(), uuidv4()];
|
|
|
|
// Create agent
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Test Agent',
|
|
author: authorId,
|
|
model: 'gpt-4',
|
|
provider: 'openai',
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileIds[0]], // Only one file attached
|
|
},
|
|
},
|
|
});
|
|
|
|
// Check access as the author
|
|
const { hasAccessToFilesViaAgent } = require('./File');
|
|
const accessMap = await hasAccessToFilesViaAgent(authorId, fileIds, agentId);
|
|
|
|
// Author should have access to all files
|
|
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().toString();
|
|
const fileIds = [uuidv4(), uuidv4()];
|
|
|
|
const { hasAccessToFilesViaAgent } = require('./File');
|
|
const accessMap = await hasAccessToFilesViaAgent(userId, fileIds, 'non-existent-agent');
|
|
|
|
// Should have no access to any files
|
|
expect(accessMap.get(fileIds[0])).toBe(false);
|
|
expect(accessMap.get(fileIds[1])).toBe(false);
|
|
});
|
|
|
|
it('should deny access when agent is not collaborative', async () => {
|
|
const userId = new mongoose.Types.ObjectId().toString();
|
|
const authorId = new mongoose.Types.ObjectId().toString();
|
|
const agentId = uuidv4();
|
|
const fileIds = [uuidv4(), uuidv4()];
|
|
|
|
// Create agent with files but isCollaborative: false
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Non-Collaborative Agent',
|
|
author: authorId,
|
|
model: 'gpt-4',
|
|
provider: 'openai',
|
|
isCollaborative: false,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: fileIds,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Get or create global project
|
|
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, '_id');
|
|
|
|
// Share agent globally
|
|
await Agent.updateOne({ id: agentId }, { $push: { projectIds: globalProject._id } });
|
|
|
|
// Check access for files
|
|
const { hasAccessToFilesViaAgent } = require('./File');
|
|
const accessMap = await hasAccessToFilesViaAgent(userId, fileIds, agentId);
|
|
|
|
// Should have no access to any files when isCollaborative is false
|
|
expect(accessMap.get(fileIds[0])).toBe(false);
|
|
expect(accessMap.get(fileIds[1])).toBe(false);
|
|
});
|
|
});
|
|
|
|
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()}`;
|
|
|
|
// Create/get global project using getProjectByName which will upsert
|
|
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME);
|
|
|
|
// Create agent with shared file
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Shared Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
projectIds: [globalProject._id],
|
|
isCollaborative: true,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [sharedFileId],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create files
|
|
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,
|
|
});
|
|
|
|
// Get files with access control
|
|
const files = await getFiles(
|
|
{ file_id: { $in: [ownedFileId, sharedFileId, inaccessibleFileId] } },
|
|
null,
|
|
{ text: 0 },
|
|
{ userId: userId.toString(), 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);
|
|
});
|
|
});
|
|
});
|