LibreChat/api/server/middleware/accessResources/fileAccess.js
Danny Avila 5c338a4642
🛂 fix: Harden Agent File Preview Access (#12981)
* fix: harden agent file access

* style: format agent file query

* fix: prune agent file refs on alternate writes

* test: fix agent pruning specs
2026-05-06 19:56:04 -04:00

145 lines
4.3 KiB
JavaScript

const { logger } = require('@librechat/data-schemas');
const { PermissionBits, hasPermissions, ResourceType } = require('librechat-data-provider');
const { getEffectivePermissions } = require('~/server/services/PermissionService');
const { getAgents, getFiles } = require('~/models');
/**
* Checks if user has access to a file through agent permissions
* Files inherit permissions from agents authored by the file owner
*/
const checkAgentBasedFileAccess = async ({ userId, role, fileId, fileOwner }) => {
try {
/** Agents that have this file in their tool_resources */
const agentsWithFile = await getAgents({
$or: [
{ 'tool_resources.execute_code.file_ids': fileId },
{ 'tool_resources.file_search.file_ids': fileId },
{ 'tool_resources.image_edit.file_ids': fileId },
{ 'tool_resources.context.file_ids': fileId },
{ 'tool_resources.ocr.file_ids': fileId },
],
});
if (!agentsWithFile || agentsWithFile.length === 0) {
return false;
}
const fileOwnerId = fileOwner?.toString();
const userIdStr = userId.toString();
for (const agent of agentsWithFile) {
const agentAuthorId = agent.author?.toString();
if (!agentAuthorId || !fileOwnerId || agentAuthorId !== fileOwnerId) {
continue;
}
if (agentAuthorId === userIdStr) {
logger.debug(`[fileAccess] User is author of agent ${agent.id}`);
return true;
}
try {
const permissions = await getEffectivePermissions({
userId,
role,
resourceType: ResourceType.AGENT,
resourceId: agent._id || agent.id,
});
if (hasPermissions(permissions, PermissionBits.VIEW)) {
logger.debug(`[fileAccess] User ${userId} has VIEW permissions on agent ${agent.id}`);
return true;
}
} catch (permissionError) {
logger.warn(
`[fileAccess] Permission check failed for agent ${agent.id}:`,
permissionError.message,
);
}
}
return false;
} catch (error) {
logger.error('[fileAccess] Error checking agent-based access:', error);
return false;
}
};
const getTenantId = (value) => value?.toString?.() ?? null;
const denyFileAccess = (res) =>
res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions to access this file',
});
/**
* Middleware to check if user can access a file
* Checks: 1) File ownership, 2) Agent-based access through a file-owner agent
*/
const fileAccess = async (req, res, next) => {
try {
const fileId = req.params.file_id;
const userId = req.user?.id;
const userRole = req.user?.role;
if (!fileId) {
return res.status(400).json({
error: 'Bad Request',
message: 'file_id is required',
});
}
if (!userId) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required',
});
}
const [file] = await getFiles({ file_id: fileId });
if (!file) {
return res.status(404).json({
error: 'Not Found',
message: 'File not found',
});
}
const fileTenantId = getTenantId(file.tenantId);
const userTenantId = getTenantId(req.user?.tenantId);
// Tenant-scoped files are restricted to their tenant. Legacy files without
// tenantId remain governed by owner/agent ACLs for non-tenant migrations.
if (fileTenantId && fileTenantId !== userTenantId) {
logger.warn(`[fileAccess] User ${userId} denied cross-tenant access to file ${fileId}`);
return denyFileAccess(res);
}
if (file.user && file.user.toString() === userId) {
req.fileAccess = { file };
return next();
}
/** Agent-based access (file inherits agent permissions) */
const hasAgentAccess = await checkAgentBasedFileAccess({
userId,
role: userRole,
fileId,
fileOwner: file.user,
});
if (hasAgentAccess) {
req.fileAccess = { file };
return next();
}
logger.warn(`[fileAccess] User ${userId} denied access to file ${fileId}`);
return denyFileAccess(res);
} catch (error) {
logger.error('[fileAccess] Error checking file access:', error);
return res.status(500).json({
error: 'Internal Server Error',
message: 'Failed to check file access permissions',
});
}
};
module.exports = {
fileAccess,
};