mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-16 20:56:35 +01:00
* 🔏 fix: Apply agent access control filtering to context/OCR resource loading
The context/OCR file path in primeResources fetched files by file_id
without applying filterFilesByAgentAccess, unlike the file_search and
execute_code paths. Add filterFiles dependency injection to primeResources
and invoke it after getFiles to enforce consistent access control.
* fix: Wire filterFilesByAgentAccess into all agent initialization callers
Pass the filterFilesByAgentAccess function from the JS layer into the TS
initializeAgent → primeResources chain via dependency injection, covering
primary, handoff, added-convo, and memory agent init paths.
* test: Add access control filtering tests for primeResources
Cover filterFiles invocation with context/OCR files, verify filtering
rejects inaccessible files, and confirm graceful fallback when filterFiles,
userId, or agentId are absent.
* fix: Guard filterFilesByAgentAccess against ephemeral agent IDs
Ephemeral agents have no DB document, so getAgent returns null and the
access map defaults to all-false, silently blocking all non-owned files.
Short-circuit with isEphemeralAgentId to preserve the pass-through
behavior for inline-built agents (memory, tool agents).
* fix: Clean up resources.ts and JS caller import order
Remove redundant optional chain on req.user.role inside user-guarded
block, update primeResources JSDoc with filterFiles and agentId params,
and reorder JS imports to longest-to-shortest per project conventions.
* test: Strengthen OCR assertion and add filterFiles error-path test
Use toHaveBeenCalledWith for the OCR filtering test to verify exact
arguments after the OCR→context merge step. Add test for filterFiles
rejection to verify graceful degradation (logs error, returns original
tool_resources).
* fix: Correct import order in addedConvo.js and initialize.js
Sort by total line length descending: loadAddedAgent (91) before
filterFilesByAgentAccess (84), loadAgentTools (91) before
filterFilesByAgentAccess (84).
* test: Add unit tests for filterFilesByAgentAccess and hasAccessToFilesViaAgent
Cover every branch in permissions.js: ephemeral agent guard, missing
userId/agentId/files early returns, all-owned short-circuit, mixed
owned + non-owned with VIEW/no-VIEW, agent-not-found fail-closed,
author path scoped to attached files, EDIT gate on delete, DB error
fail-closed, and agent with no tool_resources.
* test: Cover file.user undefined/null in permissions spec
Files with no user field fall into the non-owned path and get run
through hasAccessToFilesViaAgent. Add two cases: attached file with
no user field is returned, unattached file with no user field is
excluded.
140 lines
4.3 KiB
JavaScript
140 lines
4.3 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const { PermissionBits, ResourceType, isEphemeralAgentId } = require('librechat-data-provider');
|
|
const { checkPermission } = require('~/server/services/PermissionService');
|
|
const { getAgent } = require('~/models/Agent');
|
|
|
|
/**
|
|
* @param {Object} agent - The agent document (lean)
|
|
* @returns {Set<string>} All file IDs attached across all resource types
|
|
*/
|
|
function getAttachedFileIds(agent) {
|
|
const attachedFileIds = new Set();
|
|
if (agent.tool_resources) {
|
|
for (const resource of Object.values(agent.tool_resources)) {
|
|
if (resource?.file_ids && Array.isArray(resource.file_ids)) {
|
|
for (const fileId of resource.file_ids) {
|
|
attachedFileIds.add(fileId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return attachedFileIds;
|
|
}
|
|
|
|
/**
|
|
* Checks if a user has access to multiple files through a shared agent (batch operation).
|
|
* Access is always scoped to files actually attached to the agent's tool_resources.
|
|
* @param {Object} params - Parameters object
|
|
* @param {string} params.userId - The user ID to check access for
|
|
* @param {string} [params.role] - Optional user role to avoid DB query
|
|
* @param {string[]} params.fileIds - Array of file IDs to check
|
|
* @param {string} params.agentId - The agent ID that might grant access
|
|
* @param {boolean} [params.isDelete] - Whether the operation is a delete operation
|
|
* @returns {Promise<Map<string, boolean>>} Map of fileId to access status
|
|
*/
|
|
const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId, isDelete }) => {
|
|
const accessMap = new Map();
|
|
|
|
fileIds.forEach((fileId) => accessMap.set(fileId, false));
|
|
|
|
try {
|
|
const agent = await getAgent({ id: agentId });
|
|
|
|
if (!agent) {
|
|
return accessMap;
|
|
}
|
|
|
|
const attachedFileIds = getAttachedFileIds(agent);
|
|
|
|
if (agent.author.toString() === userId.toString()) {
|
|
fileIds.forEach((fileId) => {
|
|
if (attachedFileIds.has(fileId)) {
|
|
accessMap.set(fileId, true);
|
|
}
|
|
});
|
|
return accessMap;
|
|
}
|
|
|
|
const hasViewPermission = await checkPermission({
|
|
userId,
|
|
role,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
requiredPermission: PermissionBits.VIEW,
|
|
});
|
|
|
|
if (!hasViewPermission) {
|
|
return accessMap;
|
|
}
|
|
|
|
if (isDelete) {
|
|
const hasEditPermission = await checkPermission({
|
|
userId,
|
|
role,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
requiredPermission: PermissionBits.EDIT,
|
|
});
|
|
|
|
if (!hasEditPermission) {
|
|
return accessMap;
|
|
}
|
|
}
|
|
|
|
fileIds.forEach((fileId) => {
|
|
if (attachedFileIds.has(fileId)) {
|
|
accessMap.set(fileId, true);
|
|
}
|
|
});
|
|
|
|
return accessMap;
|
|
} catch (error) {
|
|
logger.error('[hasAccessToFilesViaAgent] Error checking file access:', error);
|
|
return accessMap;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Filter files based on user access through agents
|
|
* @param {Object} params - Parameters object
|
|
* @param {Array<MongoFile>} params.files - Array of file documents
|
|
* @param {string} params.userId - User ID for access control
|
|
* @param {string} [params.role] - Optional user role to avoid DB query
|
|
* @param {string} params.agentId - Agent ID that might grant access to files
|
|
* @returns {Promise<Array<MongoFile>>} Filtered array of accessible files
|
|
*/
|
|
const filterFilesByAgentAccess = async ({ files, userId, role, agentId }) => {
|
|
if (!userId || !agentId || !files || files.length === 0 || isEphemeralAgentId(agentId)) {
|
|
return files;
|
|
}
|
|
|
|
// Separate owned files from files that need access check
|
|
const filesToCheck = [];
|
|
const ownedFiles = [];
|
|
|
|
for (const file of files) {
|
|
if (file.user && file.user.toString() === userId.toString()) {
|
|
ownedFiles.push(file);
|
|
} else {
|
|
filesToCheck.push(file);
|
|
}
|
|
}
|
|
|
|
if (filesToCheck.length === 0) {
|
|
return ownedFiles;
|
|
}
|
|
|
|
// Batch check access for all non-owned files
|
|
const fileIds = filesToCheck.map((f) => f.file_id);
|
|
const accessMap = await hasAccessToFilesViaAgent({ userId, role, fileIds, agentId });
|
|
|
|
// Filter files based on access
|
|
const accessibleFiles = filesToCheck.filter((file) => accessMap.get(file.file_id));
|
|
|
|
return [...ownedFiles, ...accessibleFiles];
|
|
};
|
|
|
|
module.exports = {
|
|
hasAccessToFilesViaAgent,
|
|
filterFilesByAgentAccess,
|
|
};
|