const { logger } = require('@librechat/data-schemas'); const { SystemRoles } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); /** * Generic base middleware factory that creates middleware to check resource access permissions. * This middleware expects MongoDB ObjectIds as resource identifiers for ACL permission checks. * * @param {Object} options - Configuration options * @param {string} options.resourceType - The type of resource (e.g., 'agent', 'file', 'project') * @param {number} options.requiredPermission - The permission bit required (1=view, 2=edit, 4=delete, 8=share) * @param {string} [options.resourceIdParam='resourceId'] - The name of the route parameter containing the resource ID * @param {Function} [options.idResolver] - Optional function to resolve custom IDs to ObjectIds * @returns {Function} Express middleware function * * @example * // Direct usage with ObjectId (for resources that use MongoDB ObjectId in routes) * router.get('/prompts/:promptId', * canAccessResource({ resourceType: 'prompt', requiredPermission: 1 }), * getPrompt * ); * * @example * // Usage with custom ID resolver (for resources that use custom string IDs) * router.get('/agents/:id', * canAccessResource({ * resourceType: 'agent', * requiredPermission: 1, * resourceIdParam: 'id', * idResolver: (customId) => resolveAgentId(customId) * }), * getAgent * ); */ const canAccessResource = (options) => { const { resourceType, requiredPermission, resourceIdParam = 'resourceId', idResolver = null, } = options; if (!resourceType || typeof resourceType !== 'string') { throw new Error('canAccessResource: resourceType is required and must be a string'); } if (!requiredPermission || typeof requiredPermission !== 'number') { throw new Error('canAccessResource: requiredPermission is required and must be a number'); } return async (req, res, next) => { try { // Extract resource ID from route parameters const rawResourceId = req.params[resourceIdParam]; if (!rawResourceId) { logger.warn(`[canAccessResource] Missing ${resourceIdParam} in route parameters`); return res.status(400).json({ error: 'Bad Request', message: `${resourceIdParam} is required`, }); } // Check if user is authenticated if (!req.user || !req.user.id) { logger.warn( `[canAccessResource] Unauthenticated request for ${resourceType} ${rawResourceId}`, ); return res.status(401).json({ error: 'Unauthorized', message: 'Authentication required', }); } // if system admin let through if (req.user.role === SystemRoles.ADMIN) { return next(); } const userId = req.user.id; let resourceId = rawResourceId; let resourceInfo = null; // Resolve custom ID to ObjectId if resolver is provided if (idResolver) { logger.debug( `[canAccessResource] Resolving ${resourceType} custom ID ${rawResourceId} to ObjectId`, ); const resolutionResult = await idResolver(rawResourceId); if (!resolutionResult) { logger.warn(`[canAccessResource] ${resourceType} not found: ${rawResourceId}`); return res.status(404).json({ error: 'Not Found', message: `${resourceType} not found`, }); } // Handle different resolver return formats if (typeof resolutionResult === 'string' || resolutionResult._id) { resourceId = resolutionResult._id || resolutionResult; resourceInfo = typeof resolutionResult === 'object' ? resolutionResult : null; } else { resourceId = resolutionResult; } logger.debug( `[canAccessResource] Resolved ${resourceType} ${rawResourceId} to ObjectId ${resourceId}`, ); } // Check permissions using PermissionService with ObjectId const hasPermission = await checkPermission({ userId, resourceType, resourceId, requiredPermission, }); if (hasPermission) { logger.debug( `[canAccessResource] User ${userId} has permission ${requiredPermission} on ${resourceType} ${rawResourceId} (${resourceId})`, ); req.resourceAccess = { resourceType, resourceId, // MongoDB ObjectId for ACL operations customResourceId: rawResourceId, // Original ID from route params permission: requiredPermission, userId, ...(resourceInfo && { resourceInfo }), }; return next(); } logger.warn( `[canAccessResource] User ${userId} denied access to ${resourceType} ${rawResourceId} ` + `(required permission: ${requiredPermission})`, ); return res.status(403).json({ error: 'Forbidden', message: `Insufficient permissions to access this ${resourceType}`, }); } catch (error) { logger.error(`[canAccessResource] Error checking access for ${resourceType}:`, error); return res.status(500).json({ error: 'Internal Server Error', message: 'Failed to check resource access permissions', }); } }; }; module.exports = { canAccessResource, };