diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index bcd3184a993..35cf4772426 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -5176,6 +5176,7 @@ async def get_member_team_ids( VALID_EXPIRES_FILTER_VALUES = frozenset({"active", "expired"}) +USER_EMAIL_KEY_FILTER_MAX_USERS = 1000 @router.get( @@ -5195,7 +5196,7 @@ async def list_keys( ), user_email: str | None = Query( None, - description="Filter keys by the owning user's email. Case-insensitive substring match against the user table; only keys whose user_id belongs to a matching user are returned.", + description="Filter keys by the owning user's email. Case-insensitive substring match against the user table, capped at the first 1000 matching users; only keys whose user_id belongs to a matching user are returned.", ), team_id: Optional[str] = Query(None, description="Filter keys by team ID"), organization_id: Optional[str] = Query(None, description="Filter keys by organization ID"), @@ -5329,7 +5330,8 @@ async def list_keys( [ user.user_id for user in await UserRepository(prisma_client).table.find_many( - where={"user_email": {"contains": user_email, "mode": "insensitive"}} + where={"user_email": {"contains": user_email, "mode": "insensitive"}}, + take=USER_EMAIL_KEY_FILTER_MAX_USERS, ) ] if user_email and isinstance(user_email, str) diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index c1b24ccea56..329f0dd3bf1 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -6354,6 +6354,9 @@ async def test_list_keys_user_email_resolves_to_user_ids(): assert mock_find_many.call_args.kwargs["where"] == { "user_email": {"contains": "alias@example.com", "mode": "insensitive"} } + assert mock_find_many.call_args.kwargs["take"] == 1000, ( + "user lookup must be bounded; a broad substring like '@' matches every user" + ) assert mock_list_key_helper.call_args.kwargs["user_ids_for_email"] == ["user-1", "user-2"] mock_find_many.return_value = [] diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2fd76591f8d..af06c4eed3a 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -42813,7 +42813,7 @@ export interface operations { size?: number; /** @description Filter keys by user ID. Exact match by default; set substring_matching=true (admin only) for case-insensitive substring matching. */ user_id?: string | null; - /** @description Filter keys by the owning user's email. Case-insensitive substring match against the user table; only keys whose user_id belongs to a matching user are returned. */ + /** @description Filter keys by the owning user's email. Case-insensitive substring match against the user table, capped at the first 1000 matching users; only keys whose user_id belongs to a matching user are returned. */ user_email?: string | null; /** @description Filter keys by team ID */ team_id?: string | null;