fix(proxy): only a provably missing user row counts as unrestricted in the websocket passthrough gate

This commit is contained in:
mateo-berri 2026-09-04 19:35:08 -07:00
parent f846388bb1
commit a0b2e7fca6
2 changed files with 36 additions and 3 deletions

View file

@ -2352,6 +2352,13 @@ async def _backfill_null_user_email(
return updated_row
class UserNotFoundError(ValueError):
"""The user row is provably absent, as opposed to merely unreadable, so a caller that reads a missing row as no user-level limits can key on it without also swallowing a database that would not answer."""
def __init__(self, user_id: str) -> None:
super().__init__(f"User doesn't exist in db. 'user_id'={user_id}. Create user via `/user/new` call.")
@log_db_metrics
async def get_user_object(
user_id: str | None,
@ -2457,7 +2464,7 @@ async def get_user_object(
value=None,
last_db_access_time=last_db_access_time,
)
raise Exception
raise UserNotFoundError(user_id=user_id)
if response.organization_memberships is not None and len(response.organization_memberships) > 0:
# dump each organization membership to type LiteLLM_OrganizationMembershipTable
@ -2493,7 +2500,9 @@ async def get_user_object(
)
return _response
except Exception as e: # if user not in db
except UserNotFoundError:
raise
except Exception as e:
_log_budget_lookup_failure("user", e)
raise ValueError(
f"User doesn't exist in db. 'user_id'={user_id}. Create user via `/user/new` call. Got error - {e}"
@ -4169,7 +4178,7 @@ async def _user_object_or_none(
user_id_upsert=False,
proxy_logging_obj=proxy_logging_obj,
)
except ValueError:
except UserNotFoundError:
return None

View file

@ -7361,6 +7361,30 @@ async def test_enforced_model_allowlists_treats_a_missing_user_row_as_unrestrict
assert [list(scope) for scope in scopes] == [[], [], [], [], []]
class _UnreachableUserPrisma:
class db:
class litellm_usertable:
@staticmethod
async def find_unique(where: dict[str, str], include: dict[str, bool]) -> None:
raise RuntimeError("database gone")
@pytest.mark.asyncio
async def test_enforced_model_allowlists_surfaces_a_failed_user_lookup():
from litellm.proxy.auth.auth_checks import enforced_model_allowlists
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
from litellm.proxy.utils import ProxyLogging
cache = UserApiKeyCache()
with pytest.raises(ValueError, match="database gone"):
await enforced_model_allowlists(
valid_token=UserAPIKeyAuth(token="hashed-fake", user_id="user-fake"),
prisma_client=_UnreachableUserPrisma(),
user_api_key_cache=cache,
proxy_logging_obj=ProxyLogging(user_api_key_cache=cache),
)
@pytest.mark.asyncio
async def test_enforced_model_allowlists_reads_every_level_from_cache():
from litellm.proxy._types import (