diff --git a/api/server/middleware/checkPeoplePickerAccess.js b/api/server/middleware/checkPeoplePickerAccess.js new file mode 100644 index 0000000000..b2931608ff --- /dev/null +++ b/api/server/middleware/checkPeoplePickerAccess.js @@ -0,0 +1,72 @@ +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { getRoleByName } = require('~/models/Role'); +const { logger } = require('~/config'); + +/** + * Middleware to check if user has permission to access people picker functionality + * Checks specific permission based on the 'type' query parameter: + * - type=user: requires VIEW_USERS permission + * - type=group: requires VIEW_GROUPS permission + * - no type (mixed search): requires either VIEW_USERS OR VIEW_GROUPS + */ +const checkPeoplePickerAccess = async (req, res, next) => { + try { + const user = req.user; + if (!user || !user.role) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'Authentication required', + }); + } + + const role = await getRoleByName(user.role); + if (!role || !role.permissions) { + return res.status(403).json({ + error: 'Forbidden', + message: 'No permissions configured for user role', + }); + } + + const { type } = req.query; + const peoplePickerPerms = role.permissions[PermissionTypes.PEOPLE_PICKER] || {}; + const canViewUsers = peoplePickerPerms[Permissions.VIEW_USERS] === true; + const canViewGroups = peoplePickerPerms[Permissions.VIEW_GROUPS] === true; + + if (type === 'user') { + if (!canViewUsers) { + return res.status(403).json({ + error: 'Forbidden', + message: 'Insufficient permissions to search for users', + }); + } + } else if (type === 'group') { + if (!canViewGroups) { + return res.status(403).json({ + error: 'Forbidden', + message: 'Insufficient permissions to search for groups', + }); + } + } else { + if (!canViewUsers || !canViewGroups) { + return res.status(403).json({ + error: 'Forbidden', + message: 'Insufficient permissions to search for both users and groups', + }); + } + } + next(); + } catch (error) { + logger.error( + `[checkPeoplePickerAccess][${req.user?.id}] checkPeoplePickerAccess error for req.query.type = ${req.query.type}`, + error, + ); + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to check permissions', + }); + } +}; + +module.exports = { + checkPeoplePickerAccess, +}; diff --git a/api/server/routes/accessPermissions.js b/api/server/routes/accessPermissions.js index e5720de81f..814fa233ca 100644 --- a/api/server/routes/accessPermissions.js +++ b/api/server/routes/accessPermissions.js @@ -8,6 +8,7 @@ const { searchPrincipals, } = require('~/server/controllers/PermissionsController'); const { requireJwtAuth, checkBan, uaParser, canAccessResource } = require('~/server/middleware'); +const { checkPeoplePickerAccess } = require('~/server/middleware/checkPeoplePickerAccess'); const router = express.Router(); @@ -25,7 +26,7 @@ router.use(uaParser); * GET /api/permissions/search-principals * Search for users and groups to grant permissions */ -router.get('/search-principals', searchPrincipals); +router.get('/search-principals', checkPeoplePickerAccess, searchPrincipals); /** * GET /api/permissions/{resourceType}/roles diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 5c08b1af2e..a8800dff51 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -51,6 +51,16 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol runCode: interfaceConfig?.runCode ?? defaults.runCode, webSearch: interfaceConfig?.webSearch ?? defaults.webSearch, customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome, + peoplePicker: { + admin: { + users: interfaceConfig?.peoplePicker?.admin?.users ?? defaults.peoplePicker.admin.users, + groups: interfaceConfig?.peoplePicker?.admin?.groups ?? defaults.peoplePicker.admin.groups, + }, + user: { + users: interfaceConfig?.peoplePicker?.user?.users ?? defaults.peoplePicker.user.users, + groups: interfaceConfig?.peoplePicker?.user?.groups ?? defaults.peoplePicker.user.groups, + }, + }, }); await updateAccessPermissions(roleName, { @@ -65,6 +75,10 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: loadedInterface.peoplePicker.user?.users, + [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.user?.groups, + }, }); await updateAccessPermissions(SystemRoles.ADMIN, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, @@ -78,6 +92,10 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, + [PermissionTypes.PEOPLE_PICKER]: { + [Permissions.VIEW_USERS]: loadedInterface.peoplePicker.admin?.users, + [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.admin?.groups, + }, }); let i = 0; diff --git a/client/src/components/SidePanel/Agents/Sharing/GrantAccessDialog.tsx b/client/src/components/SidePanel/Agents/Sharing/GrantAccessDialog.tsx index bef9131e62..0a0387b9b3 100644 --- a/client/src/components/SidePanel/Agents/Sharing/GrantAccessDialog.tsx +++ b/client/src/components/SidePanel/Agents/Sharing/GrantAccessDialog.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react'; -import { ACCESS_ROLE_IDS } from 'librechat-data-provider'; +import { ACCESS_ROLE_IDS, PermissionTypes, Permissions } from 'librechat-data-provider'; import type { TPrincipal } from 'librechat-data-provider'; import { Button, @@ -12,7 +12,7 @@ import { } from '~/components/ui'; import { cn, removeFocusOutlines } from '~/utils'; import { useToastContext } from '~/Providers'; -import { useLocalize, useCopyToClipboard } from '~/hooks'; +import { useLocalize, useCopyToClipboard, useHasAccess } from '~/hooks'; import { useGetResourcePermissionsQuery, useUpdateResourcePermissionsMutation, @@ -39,6 +39,29 @@ export default function GrantAccessDialog({ const localize = useLocalize(); const { showToast } = useToastContext(); + // Check if user has permission to access people picker + const canViewUsers = useHasAccess({ + permissionType: PermissionTypes.PEOPLE_PICKER, + permission: Permissions.VIEW_USERS, + }); + const canViewGroups = useHasAccess({ + permissionType: PermissionTypes.PEOPLE_PICKER, + permission: Permissions.VIEW_GROUPS, + }); + const hasPeoplePickerAccess = canViewUsers || canViewGroups; + + // Determine type filter based on permissions + const peoplePickerTypeFilter = useMemo(() => { + if (canViewUsers && canViewGroups) { + return null; // Both types allowed + } else if (canViewUsers) { + return 'user' as const; + } else if (canViewGroups) { + return 'group' as const; + } + return null; + }, [canViewUsers, canViewGroups]); + const { data: permissionsData, // isLoading: isLoadingPermissions, @@ -178,26 +201,31 @@ export default function GrantAccessDialog({
- + {hasPeoplePickerAccess && ( + <> + -
-
-
- - +
+
+
+ + +
+
+
-
- -
+ + )}
- + {hasPeoplePickerAccess && ( + + )} {agentId && (