From 7ef8a057ffc10f31b130cd1a5b476661a991d540 Mon Sep 17 00:00:00 2001 From: shivam Date: Fri, 4 Sep 2026 22:37:21 +0000 Subject: [PATCH 1/2] fix(jwt): keep team auto-join non-blocking for the caller's request Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/auth/handle_jwt.py | 29 +++++--- .../proxy/auth/test_handle_jwt.py | 70 +++++++++++++++---- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index 0795cee7409..7b44bd1846e 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -1260,6 +1260,14 @@ class JWTHandler: await self.http_handler.close() +def _is_team_auto_join_denied(error: Exception) -> bool: + if isinstance(error, ProxyException): + return error.code == "403" + if isinstance(error, HTTPException): + return error.status_code == 403 + return False + + class JWTAuthManager: """Manages JWT authentication and authorization operations""" @@ -1878,25 +1886,28 @@ class JWTAuthManager: ), team_id=team_object.team_id, ) - # add user to team - make this non-blocking to avoid authentication failures try: await team_member_add( data=data, - user_api_key_dict=UserAPIKeyAuth( - user_role=LitellmUserRoles.PROXY_ADMIN - ), # [TODO]: expose an internal service role, for better tracking - ) + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), + ) # [TODO]: expose an internal service role, for better tracking verbose_proxy_logger.debug( "Successfully added user %s to team %s", user_object.user_id, team_object.team_id ) - except ProxyException as e: - if e.type == ProxyErrorTypes.team_member_already_in_team: + except Exception as e: + if _is_team_auto_join_denied(e): + raise + if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.team_member_already_in_team: verbose_proxy_logger.debug( "User %s is already a member of team %s", user_object.user_id, team_object.team_id ) return - else: - raise e + verbose_proxy_logger.warning( + "JWT auto-join: could not add user %s to team %s, continuing the request: %s", + user_object.user_id, + team_object.team_id, + e, + ) return @staticmethod diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index 99a0a4c0a8b..28a7d53bac7 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -106,30 +106,72 @@ async def test_map_user_to_teams_handles_already_in_team_exception(): @pytest.mark.asyncio -async def test_map_user_to_teams_reraises_other_proxy_exceptions(): - """Test that other ProxyException types are re-raised""" +@pytest.mark.parametrize( + "side_effect", + [ + ProxyException( + message="Some other error", + type=ProxyErrorTypes.internal_server_error, + param=None, + code="500", + ), + HTTPException( + status_code=500, + detail={"error": "Unique constraint failed on the fields: (`user_id`,`team_id`)"}, + ), + RuntimeError("db pool timeout"), + ], +) +async def test_map_user_to_teams_swallows_non_auth_failures(side_effect): + """Test that non-authorization failures do not reject the request""" # Setup test data user = LiteLLM_UserTable(user_id="test_user_1") team = LiteLLM_TeamTable(team_id="test_team_1", members_with_roles=[]) - # Create a ProxyException with a different error type - other_exception = ProxyException( - message="Some other error", - type=ProxyErrorTypes.internal_server_error, - param="some_param", - code="500", - ) - - # Mock team_member_add to raise the exception with patch( "litellm.proxy.management_endpoints.team_endpoints.team_member_add", new_callable=AsyncMock, - side_effect=other_exception, + side_effect=side_effect, ) as mock_add: - # This should re-raise the exception - with pytest.raises(ProxyException) as exc_info: + with patch("litellm.proxy.auth.handle_jwt.verbose_proxy_logger") as mock_logger: + result = await JWTAuthManager.map_user_to_teams(user_object=user, team_object=team) + + assert result is None + mock_add.assert_called_once() + mock_logger.warning.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "side_effect, exception_type", + [ + ( + ProxyException( + message="forbidden", + type=ProxyErrorTypes.auth_error, + param=None, + code="403", + ), + ProxyException, + ), + (HTTPException(status_code=403, detail="forbidden"), HTTPException), + ], +) +async def test_map_user_to_teams_reraises_authorization_denials(side_effect, exception_type): + """Test that authorization denials reject the request""" + user = LiteLLM_UserTable(user_id="test_user_1") + team = LiteLLM_TeamTable(team_id="test_team_1", members_with_roles=[]) + + with patch( + "litellm.proxy.management_endpoints.team_endpoints.team_member_add", + new_callable=AsyncMock, + side_effect=side_effect, + ) as mock_add: + with pytest.raises(exception_type): await JWTAuthManager.map_user_to_teams(user_object=user, team_object=team) + mock_add.assert_called_once() + @pytest.mark.asyncio async def test_map_user_to_teams_null_inputs(): From 6771e8dffc28185c33f7ac616f2a64f7b5606177 Mon Sep 17 00:00:00 2001 From: shivam Date: Fri, 4 Sep 2026 23:01:08 +0000 Subject: [PATCH 2/2] test(jwt): document intentional internal patches Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/test_litellm/proxy/auth/test_handle_jwt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index 28a7d53bac7..d1c9b706f9b 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -128,12 +128,14 @@ async def test_map_user_to_teams_swallows_non_auth_failures(side_effect): user = LiteLLM_UserTable(user_id="test_user_1") team = LiteLLM_TeamTable(team_id="test_team_1", members_with_roles=[]) - with patch( + with patch( # test-quality-ok: map_user_to_teams imports team_member_add locally "litellm.proxy.management_endpoints.team_endpoints.team_member_add", new_callable=AsyncMock, side_effect=side_effect, ) as mock_add: - with patch("litellm.proxy.auth.handle_jwt.verbose_proxy_logger") as mock_logger: + with patch( # test-quality-ok: assert the warning emitted for swallowed failures + "litellm.proxy.auth.handle_jwt.verbose_proxy_logger" + ) as mock_logger: result = await JWTAuthManager.map_user_to_teams(user_object=user, team_object=team) assert result is None