const mongoose = require('mongoose'); const express = require('express'); const { AccessRoleIds, PrincipalType, ResourceType, PermissionBits, } = require('librechat-data-provider'); const { getUserEffectivePermissions, getAllEffectivePermissions, updateResourcePermissions, getResourcePermissions, getResourceRoles, searchPrincipals, } = require('~/server/controllers/PermissionsController'); const { checkShareAccess, checkSharePublicAccess, } = require('~/server/middleware/checkSharePublicAccess'); const { requireJwtAuth, checkBan, uaParser, canAccessResource } = require('~/server/middleware'); const { checkPeoplePickerAccess } = require('~/server/middleware/checkPeoplePickerAccess'); const { findMCPServerByObjectId, getSkillById } = require('~/models'); const router = express.Router(); // Apply common middleware router.use(requireJwtAuth); router.use(checkBan); router.use(uaParser); /** * Generic routes for resource permissions * Pattern: /api/permissions/{resourceType}/{resourceId} */ /** * GET /api/permissions/search-principals * Search for users and groups to grant permissions */ router.get('/search-principals', checkPeoplePickerAccess, searchPrincipals); /** * GET /api/permissions/{resourceType}/roles * Get available roles for a resource type */ router.get('/:resourceType/roles', getResourceRoles); /** * Middleware factory to check resource access for permission-related operations. * SECURITY: Users must have SHARE permission to view or modify resource permissions. * @param {string} requiredPermission - The permission bit required (e.g., SHARE) * @returns Express middleware function */ const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) => { const { resourceType } = req.params; let middleware; if (resourceType === ResourceType.AGENT) { middleware = canAccessResource({ resourceType: ResourceType.AGENT, requiredPermission, resourceIdParam: 'resourceId', }); } else if (resourceType === ResourceType.REMOTE_AGENT) { middleware = canAccessResource({ resourceType: ResourceType.REMOTE_AGENT, requiredPermission, resourceIdParam: 'resourceId', }); } else if (resourceType === ResourceType.PROMPTGROUP) { middleware = canAccessResource({ resourceType: ResourceType.PROMPTGROUP, requiredPermission, resourceIdParam: 'resourceId', }); } else if (resourceType === ResourceType.MCPSERVER) { middleware = canAccessResource({ resourceType: ResourceType.MCPSERVER, requiredPermission, resourceIdParam: 'resourceId', idResolver: findMCPServerByObjectId, }); } else if (resourceType === ResourceType.SKILL) { middleware = canAccessResource({ resourceType: ResourceType.SKILL, requiredPermission, resourceIdParam: 'resourceId', idResolver: getSkillById, }); } else if (resourceType === ResourceType.SHARED_LINK) { middleware = canAccessResource({ resourceType: ResourceType.SHARED_LINK, requiredPermission, resourceIdParam: 'resourceId', }); } else { return res.status(400).json({ error: 'Bad Request', message: `Unsupported resource type: ${resourceType}`, }); } // Execute the middleware middleware(req, res, next); }; const rejectSharedLinkOwnerPermissionChanges = async (req, res, next) => { if (req.params.resourceType !== ResourceType.SHARED_LINK) { return next(); } const updated = Array.isArray(req.body?.updated) ? req.body.updated : []; const removed = Array.isArray(req.body?.removed) ? req.body.removed : []; const grantsOwner = updated.some( (principal) => principal?.accessRoleId === AccessRoleIds.SHARED_LINK_OWNER, ); const grantsPublicOwner = req.body?.publicAccessRoleId === AccessRoleIds.SHARED_LINK_OWNER; if (grantsOwner || grantsPublicOwner) { return res.status(400).json({ error: 'Bad Request', message: 'Shared link owner permissions cannot be changed', }); } const userMutations = [...updated, ...removed].filter( (principal) => principal?.type === PrincipalType.USER && principal?.id, ); if (userMutations.length === 0) { return next(); } try { const SharedLink = mongoose.models.SharedLink; const link = await SharedLink.findById(req.params.resourceId, 'user').lean(); const ownerId = link?.user?.toString(); const touchesOwner = ownerId ? userMutations.some((principal) => principal.id?.toString() === ownerId) : false; if (touchesOwner) { return res.status(400).json({ error: 'Bad Request', message: 'Shared link owner permissions cannot be changed', }); } } catch (_error) { return res.status(500).json({ error: 'Internal Server Error', message: 'Failed to validate shared link owner permissions', }); } return next(); }; /** * GET /api/permissions/{resourceType}/{resourceId} * Get all permissions for a specific resource * SECURITY: Requires SHARE permission to view resource permissions */ router.get( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), getResourcePermissions, ); /** * PUT /api/permissions/{resourceType}/{resourceId} * Bulk update permissions for a specific resource * SECURITY: Requires resource ACL SHARE and role SHARE to modify resource permissions * SECURITY: Requires SHARE_PUBLIC permission to enable public sharing */ router.put( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), checkShareAccess, checkSharePublicAccess, rejectSharedLinkOwnerPermissionChanges, updateResourcePermissions, ); /** * GET /api/permissions/{resourceType}/effective/all * Get user's effective permissions for all accessible resources of a type */ router.get('/:resourceType/effective/all', getAllEffectivePermissions); /** * GET /api/permissions/{resourceType}/{resourceId}/effective * Get user's effective permissions for a specific resource */ router.get('/:resourceType/:resourceId/effective', getUserEffectivePermissions); module.exports = router;