fix(auth): guard _team_obj_from_token against team-less tokens

The HTTPException arm in _run_centralized_common_checks assumed the
exception came from the team-object fetch, but asyncio.gather raises
the first exception from any of the five gathered coroutines. If
get_user_object / get_project_object / get_end_user_object raises
HTTPException on a token with team_id=None, the assert inside
_team_obj_from_token fires and the outer auth-exception handler
mishandles it.

Guard on team_id before calling _team_obj_from_token and default
team_object to None otherwise. (Greptile P1.)
This commit is contained in:
user 2026-04-23 03:17:49 +00:00
parent fb55e2f1b9
commit b5c78d7db4
No known key found for this signature in database
2 changed files with 56 additions and 3 deletions

View file

@ -1716,9 +1716,14 @@ async def _run_centralized_common_checks(
global_proxy_spend,
) = await asyncio.gather(*fetch_coros, return_exceptions=False)
except HTTPException:
# get_team_object raises HTTPException when the team isn't found.
# Reconstruct from the token so enforcement can still run.
team_object = _team_obj_from_token(user_api_key_auth_obj)
# Any of the five gathered fetches can raise HTTPException. Only
# reconstruct from the token when a team_id is known — otherwise
# the exception came from a different fetch and the assert in
# _team_obj_from_token would fire.
if user_api_key_auth_obj.team_id is not None:
team_object = _team_obj_from_token(user_api_key_auth_obj)
else:
team_object = None
user_object = None
project_object = None
end_user_object = None

View file

@ -2050,3 +2050,51 @@ async def test_centralized_common_checks_short_circuits_when_master_key_unset():
finally:
for k, v in originals.items():
setattr(_proxy_server_mod, k, v)
@pytest.mark.asyncio
async def test_centralized_common_checks_http_exception_without_team_id():
"""Regression: an HTTPException raised by any of the five parallel
fetches (user/project/end_user/global_spend) must not trigger the
_team_obj_from_token reconstruction when the token has no team_id
the helper asserts team_id is not None. This is the Greptile P1
finding: the ``except HTTPException`` arm was team-fetch-biased."""
import litellm.proxy.proxy_server as _proxy_server_mod
from fastapi import HTTPException, Request
from starlette.datastructures import URL
token = UserAPIKeyAuth(api_key="sk-test", user_id="u", team_id=None)
request = Request(scope={"type": "http"})
request._url = URL(url="/chat/completions")
attrs = _proxy_attrs_for_centralized_checks(user_custom_auth=None)
originals = {a: getattr(_proxy_server_mod, a, None) for a in attrs}
try:
for k, v in attrs.items():
setattr(_proxy_server_mod, k, v)
# Make the user fetch raise HTTPException. asyncio.gather with
# return_exceptions=False propagates it.
with (
patch(
"litellm.proxy.auth.user_api_key_auth.get_user_object",
new_callable=AsyncMock,
side_effect=HTTPException(status_code=404, detail="user-not-found"),
),
patch(
"litellm.proxy.auth.user_api_key_auth.common_checks",
new_callable=AsyncMock,
) as mock_checks,
):
# Should NOT raise AssertionError from _team_obj_from_token;
# should proceed with team_object=None.
await _run_centralized_common_checks(
user_api_key_auth_obj=token,
request=request,
request_data={"model": "gpt-4o"},
route="/chat/completions",
)
mock_checks.assert_awaited_once()
assert mock_checks.call_args.kwargs["team_object"] is None
finally:
for k, v in originals.items():
setattr(_proxy_server_mod, k, v)