Files
wehub-resource-sync e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:08:12 +08:00

154 lines
4.4 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 they are attached to.
*/
const checkAgentBasedFileAccess = async ({ userId, role, fileId, fileOwner }) => {
try {
const fileOwnerId = fileOwner?.toString();
if (!fileOwnerId) {
return false;
}
/** 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 userIdStr = userId.toString();
for (const agent of agentsWithFile) {
const agentAuthorId = agent.author?.toString();
if (!agentAuthorId) {
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 attached agents
*/
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} (route ${req.originalUrl})`,
);
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} (route ${req.originalUrl})`,
);
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,
};