diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 92862ed9dc4..50693850c56 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -30,7 +30,7 @@ from litellm.proxy.management_endpoints.common_daily_activity import ( get_daily_activity, get_daily_activity_aggregated, ) -from litellm.proxy.auth.auth_checks import get_user_object +from litellm.proxy.auth.auth_checks import get_team_object, get_user_object from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view from litellm.proxy.management_endpoints.key_management_endpoints import ( generate_key_helper_fn, @@ -1820,6 +1820,118 @@ async def add_internal_user_to_organization( raise Exception(f"Failed to add user to organization: {str(e)}") +async def _resolve_org_filter_for_user_search( + user_api_key_dict: UserAPIKeyAuth, + team_id: Optional[str], + prisma_client: Any, + user_api_key_cache: Any, + proxy_logging_obj: Any, +) -> Optional[List[str]]: + """ + Return a list of org IDs to filter by, or ``None`` for no filter. + + Reads the ``scope_user_search_to_org`` UI-setting flag and applies + role-based access rules when the flag is ON. + """ + from litellm.proxy.management_endpoints.common_utils import ( + _is_user_team_admin, + ) + from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( + get_ui_settings_cached, + ) + + ui_settings = await get_ui_settings_cached() + if not ui_settings.get("scope_user_search_to_org", False): + return None # flag OFF — no filtering + + if _user_has_admin_view(user_api_key_dict): + return None # proxy admin — see everything + + # Try to resolve org admin memberships + caller_user = None + if user_api_key_dict.user_id is not None: + try: + caller_user = await get_user_object( + user_id=user_api_key_dict.user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + user_id_upsert=False, + proxy_logging_obj=proxy_logging_obj, + ) + except ValueError: + caller_user = None + + org_admin_org_ids: List[str] = [] + if caller_user is not None: + org_admin_org_ids = [ + m.organization_id + for m in (caller_user.organization_memberships or []) + if m.user_role == LitellmUserRoles.ORG_ADMIN.value + ] + + if org_admin_org_ids: + return org_admin_org_ids + + if team_id is not None: + return await _resolve_team_org_filter( + user_api_key_dict, team_id, prisma_client, + user_api_key_cache, proxy_logging_obj, + ) + + raise HTTPException( + status_code=403, + detail={ + "error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users." + }, + ) + + +async def _resolve_team_org_filter( + user_api_key_dict: UserAPIKeyAuth, + team_id: str, + prisma_client: Any, + user_api_key_cache: Any, + proxy_logging_obj: Any, +) -> List[str]: + """Look up the team and return its org as a filter list, or raise 403.""" + from litellm.proxy.management_endpoints.common_utils import ( + _is_user_team_admin, + ) + + try: + team_obj = await get_team_object( + team_id=team_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + except HTTPException: + raise HTTPException( + status_code=403, + detail={ + "error": f"scope_user_search_to_org is enabled but team '{team_id}' was not found." + }, + ) + + if not _is_user_team_admin(user_api_key_dict, team_obj): + raise HTTPException( + status_code=403, + detail={ + "error": "scope_user_search_to_org is enabled. You must be an admin of this team to search users." + }, + ) + + if team_obj.organization_id: + return [team_obj.organization_id] + + raise HTTPException( + status_code=403, + detail={ + "error": "scope_user_search_to_org is enabled and this team is not part of an organization. Contact your proxy admin to adjust this setting." + }, + ) + + @router.get( "/user/filter/ui", tags=["Internal User management"], @@ -1836,6 +1948,10 @@ async def ui_view_users( user_email: Optional[str] = fastapi.Query( default=None, description="User email in the request parameters" ), + team_id: Optional[str] = fastapi.Query( + default=None, + description="Team ID — used when a team admin searches for users to add to their team", + ), page: int = fastapi.Query( default=1, description="Page number for pagination", ge=1 ), @@ -1847,19 +1963,15 @@ async def ui_view_users( """ Filter users based on partial match of user_id or email with pagination. - - Proxy admins: receive all matching users. - - Organization admins: receive only users in their own organization(s). - - Other roles: access denied (403). + Behaviour depends on the ``scope_user_search_to_org`` UI-setting flag + (stored in the ``litellm_uisettings`` table): - Args: - user_id (Optional[str]): Partial user ID to search for - user_email (Optional[str]): Partial email to search for - page (int): Page number for pagination (starts at 1) - page_size (int): Number of items per page (max 100) - user_api_key_dict (UserAPIKeyAuth): User authentication information - - Returns: - List of matching user records (LiteLLM_UserTableFiltered), scoped by org for org admins. + * **Flag OFF (default):** any authenticated user can search all users. + * **Flag ON:** + - Proxy admins see all users. + - Org admins see only users in their org(s). + - Team admins for an org-bound team see users in that org. + - Others receive a 403. """ from litellm.proxy.proxy_server import ( prisma_client, @@ -1871,51 +1983,13 @@ async def ui_view_users( raise HTTPException(status_code=500, detail={"error": "No db connected"}) try: - # Restrict by caller role: proxy admin sees all; org admin sees only their org(s); others 403 - is_proxy_admin = _user_has_admin_view(user_api_key_dict) - if not is_proxy_admin: - if user_api_key_dict.user_id is None: - raise HTTPException( - status_code=403, - detail={ - "error": "Only proxy admins and organization admins can search users." - }, - ) - try: - caller_user = await get_user_object( - user_id=user_api_key_dict.user_id, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - user_id_upsert=False, - proxy_logging_obj=proxy_logging_obj, - ) - except ValueError: - # get_user_object raises ValueError when user not found (user_id_upsert=False) - raise HTTPException( - status_code=403, - detail={ - "error": "Only proxy admins and organization admins can search users." - }, - ) - if caller_user is None: - raise HTTPException( - status_code=403, - detail={ - "error": "Only proxy admins and organization admins can search users." - }, - ) - org_admin_org_ids = [ - m.organization_id - for m in (caller_user.organization_memberships or []) - if m.user_role == LitellmUserRoles.ORG_ADMIN.value - ] - if not org_admin_org_ids: - raise HTTPException( - status_code=403, - detail={ - "error": "Only proxy admins and organization admins can search users." - }, - ) + org_filter_ids = await _resolve_org_filter_for_user_search( + user_api_key_dict=user_api_key_dict, + team_id=team_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) # Calculate offset for pagination skip = (page - 1) * page_size @@ -1935,10 +2009,10 @@ async def ui_view_users( "mode": "insensitive", # Case-insensitive search } - # Org admins: only users in their org(s) - if not is_proxy_admin: + # Apply org filter when scope_user_search_to_org is ON and caller is not proxy admin + if org_filter_ids is not None: where_conditions["organization_memberships"] = { - "some": {"organization_id": {"in": org_admin_org_ids}} + "some": {"organization_id": {"in": org_filter_ids}} } # Query users with pagination and filters diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 2f7f81a703e..076a2c3bffd 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -124,6 +124,11 @@ class UISettings(BaseModel): description="If true, team admins are exempt from the vector stores disable restriction (only takes effect when disable_vector_stores_for_internal_users is true).", ) + scope_user_search_to_org: bool = Field( + default=False, + description="If enabled, the user search endpoint (/user/filter/ui) restricts results by organization. When off, any authenticated user can search all users.", + ) + class UISettingsResponse(SettingsResponse): """Response model for UI settings""" @@ -143,6 +148,7 @@ ALLOWED_UI_SETTINGS_FIELDS = { "allow_agents_for_team_admins", "disable_vector_stores_for_internal_users", "allow_vector_stores_for_team_admins", + "scope_user_search_to_org", } # Flags that must be synced from the persisted UISettings into @@ -974,6 +980,49 @@ async def get_in_product_nudges(): return InProductNudgeResponse(is_claude_code_enabled=False) +UI_SETTINGS_CACHE_KEY = "ui_settings:settings_dict" +UI_SETTINGS_CACHE_TTL = 600 # 10 minutes + + +async def get_ui_settings_cached() -> Dict[str, Any]: + """ + Return the persisted UI settings dict, using DualCache for reads. + + Cache hit → return cached dict immediately. + Cache miss → read from DB, populate cache, return dict. + """ + from litellm.proxy.proxy_server import prisma_client, user_api_key_cache + + # 1. Try cache + cached = await user_api_key_cache.async_get_cache(key=UI_SETTINGS_CACHE_KEY) + if cached is not None and isinstance(cached, dict): + return cached + + # 2. Fallback to DB + if prisma_client is None: + return {} + + db_record = await prisma_client.db.litellm_uisettings.find_unique( + where={"id": "ui_settings"} + ) + ui_settings: Dict[str, Any] = {} + if db_record and db_record.ui_settings: + raw = db_record.ui_settings + ui_settings = json.loads(raw) if isinstance(raw, str) else dict(raw) + + # Sanitize + ui_settings = { + k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS + } + + # 3. Populate cache with TTL + await user_api_key_cache.async_set_cache( + key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL + ) + + return ui_settings + + @router.get( "/get/ui_settings", tags=["UI Settings"], @@ -1018,6 +1067,13 @@ async def get_ui_settings(): general_settings.update(_flags_to_sync) + # Refresh DualCache so other code paths (e.g. /user/filter/ui) see fresh values + from litellm.proxy.proxy_server import user_api_key_cache + + await user_api_key_cache.async_set_cache( + key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL + ) + # Build config-like object for schema helper config: Dict[str, Any] = {"litellm_settings": {"ui_settings": ui_settings}} @@ -1102,6 +1158,16 @@ async def update_ui_settings( general_settings.update(_flags_to_sync) + # Invalidate + set DualCache so subsequent reads see the new values immediately + from litellm.proxy.proxy_server import user_api_key_cache + + sanitized = { + k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS + } + await user_api_key_cache.async_set_cache( + key=UI_SETTINGS_CACHE_KEY, value=sanitized, ttl=UI_SETTINGS_CACHE_TTL + ) + return { "message": "UI settings updated successfully", "status": "success", diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 16b5feb108a..16d16c040a3 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -54,6 +54,12 @@ async def test_ui_view_users_with_null_email(mocker, caplog): mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + # Flag OFF by default + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={}, + ) + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) # Proxy admin: no org filter, no get_user_object call @@ -63,6 +69,7 @@ async def test_ui_view_users_with_null_email(mocker, caplog): ), user_id="test_user", user_email=None, + team_id=None, page=1, page_size=50, ) @@ -83,6 +90,12 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker): return [] mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + + # Flag OFF by default + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={}, + ) mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) await ui_view_users( @@ -91,6 +104,7 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker): ), user_id=None, user_email="foo", + team_id=None, page=1, page_size=50, ) @@ -99,8 +113,8 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker): @pytest.mark.asyncio async def test_ui_view_users_org_admin_filtered_by_org(mocker): """ - Org admin: find_many is called with organization_memberships filter so only users - in the caller's org(s) are returned. + Org admin with scope_user_search_to_org ON: find_many is called with + organization_memberships filter so only users in the caller's org(s) are returned. """ from litellm.proxy._types import LiteLLM_OrganizationMembershipTable @@ -116,6 +130,13 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker): return [] mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + + # Flag ON + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={"scope_user_search_to_org": True}, + ) + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock()) mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock()) @@ -143,6 +164,7 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker): user_api_key_dict=UserAPIKeyAuth(user_id="org-admin", user_role=None), user_id=None, user_email="u", + team_id=None, page=1, page_size=50, ) @@ -153,11 +175,18 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker): @pytest.mark.asyncio async def test_ui_view_users_non_org_admin_returns_403(mocker): """ - Caller is not proxy admin and not org admin: endpoint returns 403. + Flag ON, caller is not proxy admin and not org admin, no team_id: endpoint returns 403. """ from fastapi import HTTPException mock_prisma_client = mocker.MagicMock() + + # Flag ON + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={"scope_user_search_to_org": True}, + ) + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock()) mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock()) @@ -179,12 +208,227 @@ async def test_ui_view_users_non_org_admin_returns_403(mocker): user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None), user_id=None, user_email="u", + team_id=None, page=1, page_size=50, ) assert exc_info.value.status_code == 403 - assert "Only proxy admins and organization admins" in str(exc_info.value.detail) + assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +async def test_ui_view_users_flag_off_internal_user_can_search(mocker): + """ + Flag OFF (default): any authenticated user can search all users without org filtering. + """ + mock_prisma_client = mocker.MagicMock() + + async def mock_find_many(*args, **kwargs): + where = kwargs.get("where") or {} + assert "organization_memberships" not in where + return [] + + mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + + # Flag OFF + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={}, + ) + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + response = await ui_view_users( + user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None), + user_id=None, + user_email="foo", + team_id=None, + page=1, + page_size=50, + ) + + assert response == [] + + +@pytest.mark.asyncio +async def test_ui_view_users_flag_on_team_admin_org_team(mocker): + """ + Flag ON, team admin for org-bound team: org filter is applied using team's org. + """ + from litellm.proxy._types import LiteLLM_TeamTableCachedObj + + mock_prisma_client = mocker.MagicMock() + org_id = "org-456" + tid = "team-789" + + async def mock_find_many(*args, **kwargs): + where = kwargs.get("where") or {} + assert "organization_memberships" in where + assert where["organization_memberships"] == { + "some": {"organization_id": {"in": [org_id]}} + } + return [] + + mock_prisma_client.db.litellm_usertable.find_many = mock_find_many + + # Flag ON + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={"scope_user_search_to_org": True}, + ) + + # Mock get_team_object + team_obj = LiteLLM_TeamTableCachedObj( + team_id=tid, + team_alias="test-team", + organization_id=org_id, + members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}], + ) + + async def mock_get_team_object(*args, **kwargs): + return team_obj + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object", + side_effect=mock_get_team_object, + ) + + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock()) + mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock()) + + # Caller is not org admin + caller_user = mocker.MagicMock() + caller_user.organization_memberships = [] + + async def mock_get_user_object(*args, **kwargs): + return caller_user + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object", + side_effect=mock_get_user_object, + ) + + response = await ui_view_users( + user_api_key_dict=UserAPIKeyAuth(user_id="team-admin-user", user_role=None), + user_id=None, + user_email="u", + team_id=tid, + page=1, + page_size=50, + ) + + assert response == [] + + +@pytest.mark.asyncio +async def test_ui_view_users_flag_on_team_admin_non_org_team_403(mocker): + """ + Flag ON, team admin for non-org team: returns 403. + """ + from fastapi import HTTPException + from litellm.proxy._types import LiteLLM_TeamTableCachedObj + + mock_prisma_client = mocker.MagicMock() + tid = "team-no-org" + + # Flag ON + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={"scope_user_search_to_org": True}, + ) + + # Mock get_team_object — team has no organization_id + team_obj = LiteLLM_TeamTableCachedObj( + team_id=tid, + team_alias="no-org-team", + organization_id=None, + members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}], + ) + + async def mock_get_team_object(*args, **kwargs): + return team_obj + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object", + side_effect=mock_get_team_object, + ) + + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock()) + mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock()) + + # Caller is not org admin + caller_user = mocker.MagicMock() + caller_user.organization_memberships = [] + + async def mock_get_user_object(*args, **kwargs): + return caller_user + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object", + side_effect=mock_get_user_object, + ) + + with pytest.raises(HTTPException) as exc_info: + await ui_view_users( + user_api_key_dict=UserAPIKeyAuth( + user_id="team-admin-user", user_role=None + ), + user_id=None, + user_email="u", + team_id=tid, + page=1, + page_size=50, + ) + + assert exc_info.value.status_code == 403 + assert "not part of an organization" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +async def test_ui_view_users_flag_on_non_admin_no_team_id_403(mocker): + """ + Flag ON, non-admin caller without team_id: returns 403. + """ + from fastapi import HTTPException + + mock_prisma_client = mocker.MagicMock() + + # Flag ON + mocker.patch( + "litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached", + return_value={"scope_user_search_to_org": True}, + ) + + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock()) + mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock()) + + # Caller is not org admin + caller_user = mocker.MagicMock() + caller_user.organization_memberships = [] + + async def mock_get_user_object(*args, **kwargs): + return caller_user + + mocker.patch( + "litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object", + side_effect=mock_get_user_object, + ) + + with pytest.raises(HTTPException) as exc_info: + await ui_view_users( + user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None), + user_id=None, + user_email="u", + team_id=None, + page=1, + page_size=50, + ) + + assert exc_info.value.status_code == 403 + assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail) def test_user_daily_activity_types(): diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index dfc66d3484d..a22c78c9430 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -23,6 +23,7 @@ export default function UISettings() { const allowAgentsTeamAdminsProperty = schema?.properties?.allow_agents_for_team_admins; const disableVectorStoresProperty = schema?.properties?.disable_vector_stores_for_internal_users; const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins; + const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org; const values = data?.values ?? {}; const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users); const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user); @@ -167,6 +168,20 @@ export default function UISettings() { ); }; + const handleToggleScopeUserSearch = (checked: boolean) => { + updateSettings( + { scope_user_search_to_org: checked }, + { + onSuccess: () => { + NotificationManager.success("UI settings updated successfully"); + }, + onError: (error) => { + NotificationManager.fromBackend(error); + }, + }, + ); + }; + return ( {isLoading ? ( @@ -347,6 +362,26 @@ export default function UISettings() { + {/* Scope user search to organization */} + + + + Scope user search to organization + + {scopeUserSearchProperty?.description ?? + "If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."} + + + + + + {/* Page Visibility for Internal Users */} = ({ @@ -52,6 +53,7 @@ const UserSearchModal: React.FC = ({ { label: "user", value: "user", description: "User role. Can view team info, but not manage it." }, ], defaultRole = "user", + teamId, }) => { const [form] = Form.useForm(); const [userOptions, setUserOptions] = useState([]); @@ -69,6 +71,9 @@ const UserSearchModal: React.FC = ({ try { const params = new URLSearchParams(); params.append(fieldName, searchText); + if (teamId) { + params.append("team_id", teamId); + } if (accessToken == null) { return; } diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 91454d8d8b3..8d1e8ce81cd 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2473,14 +2473,19 @@ export const allEndUsersCall = async (accessToken: string) => { export const userFilterUICall = async (accessToken: string, params: URLSearchParams) => { try { - let url = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`; - + const base = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`; + const queryParams = new URLSearchParams(); if (params.get("user_email")) { - url += `?user_email=${params.get("user_email")}`; + queryParams.append("user_email", params.get("user_email")!); } if (params.get("user_id")) { - url += `?user_id=${params.get("user_id")}`; + queryParams.append("user_id", params.get("user_id")!); } + if (params.get("team_id")) { + queryParams.append("team_id", params.get("team_id")!); + } + const qs = queryParams.toString(); + const url = qs ? `${base}?${qs}` : base; const response = await fetch(url, { method: "GET", diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 62208b4186d..7d350b281e4 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -1303,6 +1303,7 @@ const TeamInfoView: React.FC = ({ onCancel={() => setIsAddMemberModalVisible(false)} onSubmit={handleMemberCreate} accessToken={accessToken} + teamId={teamId} /> {/* Delete Member Confirmation Modal */}