feat: Add role-level permissions for agent sharing people picker

- Add PEOPLE_PICKER permission type with VIEW_USERS and VIEW_GROUPS permissions
  - Create custom middleware for query-aware permission validation
  - Implement permission-based type filtering in PeoplePicker component
  - Hide people picker UI when user lacks permissions, show only public toggle
  - Support granular access: users-only, groups-only, or mixed search modes
This commit is contained in:
Atef Bellaaj 2025-07-01 14:25:48 +02:00 committed by Danny Avila
parent b03341517d
commit 73fb4181fe
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
11 changed files with 220 additions and 32 deletions

View file

@ -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({
</OGDialogTitle>
<div className="space-y-6 p-2">
<PeoplePicker
onSelectionChange={setNewShares}
placeholder={localize('com_ui_search_people_placeholder')}
/>
{hasPeoplePickerAccess && (
<>
<PeoplePicker
onSelectionChange={setNewShares}
placeholder={localize('com_ui_search_people_placeholder')}
typeFilter={peoplePickerTypeFilter}
/>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-text-secondary" />
<label className="text-sm font-medium text-text-primary">
{localize('com_ui_permission_level')}
</label>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-text-secondary" />
<label className="text-sm font-medium text-text-primary">
{localize('com_ui_permission_level')}
</label>
</div>
</div>
<AccessRolesPicker
resourceType={resourceType}
selectedRoleId={defaultPermissionId}
onRoleChange={setDefaultPermissionId}
/>
</div>
</div>
<AccessRolesPicker
resourceType={resourceType}
selectedRoleId={defaultPermissionId}
onRoleChange={setDefaultPermissionId}
/>
</div>
</>
)}
<PublicSharingToggle
isPublic={isPublic}
publicRole={publicRole}
@ -207,11 +235,13 @@ export default function GrantAccessDialog({
/>
<div className="flex justify-between border-t pt-4">
<div className="flex gap-2">
<ManagePermissionsDialog
agentDbId={agentDbId}
agentName={agentName}
resourceType={resourceType}
/>
{hasPeoplePickerAccess && (
<ManagePermissionsDialog
agentDbId={agentDbId}
agentName={agentName}
resourceType={resourceType}
/>
)}
{agentId && (
<Button
variant="outline"

View file

@ -11,12 +11,14 @@ interface PeoplePickerProps {
onSelectionChange: (principals: TPrincipal[]) => void;
placeholder?: string;
className?: string;
typeFilter?: 'user' | 'group' | null;
}
export default function PeoplePicker({
onSelectionChange,
placeholder,
className = '',
typeFilter = null,
}: PeoplePickerProps) {
const localize = useLocalize();
const [searchQuery, setSearchQuery] = useState('');
@ -26,8 +28,9 @@ export default function PeoplePicker({
() => ({
q: searchQuery,
limit: 30,
...(typeFilter && { type: typeFilter }),
}),
[searchQuery],
[searchQuery, typeFilter],
);
const {