mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(proxy): take FIPS logins one at a time and recheck the throttle first
A login that waited for the KDF thread asked the throttle again only after deriving, so a burst for one user kept that thread busy long after the user was blocked. Logins now wait for their turn on a lock and ask the throttle when it comes, so a blocked login costs no PBKDF2 and only one login at a time holds a place in the KDF queue. Password changes and invite claims no longer wait behind a whole burst of logins.
This commit is contained in:
parent
b4c0a75395
commit
fa160e0603
2 changed files with 50 additions and 5 deletions
|
|
@ -5,6 +5,7 @@ This module contains the core login logic that can be reused across different
|
|||
login endpoints (e.g., /login and /v2/login).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import secrets
|
||||
from collections.abc import Mapping
|
||||
|
|
@ -107,6 +108,18 @@ async def screen_login_password_for_breach(
|
|||
return breached
|
||||
|
||||
|
||||
_ONE_FIPS_LOGIN_AT_A_TIME: Final = asyncio.Lock()
|
||||
|
||||
|
||||
async def _password_matches(password: str, stored: str, attempt: LoginAttempt) -> bool:
|
||||
"""Checks inline, or in FIPS mode one login at a time, refusing a login whose user got blocked while it waited."""
|
||||
if not openssl_enforces_fips():
|
||||
return verify_password(password, stored)
|
||||
async with _ONE_FIPS_LOGIN_AT_A_TIME:
|
||||
await attempt.refuse_if_blocked_while_waiting()
|
||||
return await run_off_loop_if_fips(verify_password, password, stored)
|
||||
|
||||
|
||||
async def _rehash_password_if_needed(user_id: str, password: str, stored: str) -> None:
|
||||
"""Rehash legacy password (SHA256) to scrypt on successful login."""
|
||||
if stored.startswith(("scrypt:", "pbkdf2:")):
|
||||
|
|
@ -413,10 +426,7 @@ async def _sign_in(
|
|||
code=401,
|
||||
)
|
||||
|
||||
password_matches: Final = await run_off_loop_if_fips(verify_password, password, _password)
|
||||
if openssl_enforces_fips():
|
||||
await attempt.refuse_if_blocked_while_waiting()
|
||||
if password_matches:
|
||||
if await _password_matches(password, _password, attempt):
|
||||
await _rehash_password_if_needed(_user_row.user_id, password, _password)
|
||||
breached_now: Final = prisma_client is not None and await screen_login_password_for_breach(
|
||||
user_id=_user_row.user_id,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ This module tests the refactored login logic that was moved from proxy_server.py
|
|||
to login_utils.py for better reusability.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
|
|
@ -2471,7 +2472,7 @@ async def _login_whose_user_gets_blocked_during_the_row_lookup(monkeypatch: pyte
|
|||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("fips_openssl")
|
||||
async def test_fips_login_hides_the_right_password_when_its_user_got_blocked_while_it_waited(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
monkeypatch: pytest.MonkeyPatch, pbkdf2_on_event_loop: list[bool]
|
||||
) -> None:
|
||||
from litellm.proxy._types import ProxyException
|
||||
|
||||
|
|
@ -2479,6 +2480,40 @@ async def test_fips_login_hides_the_right_password_when_its_user_got_blocked_whi
|
|||
await _login_whose_user_gets_blocked_during_the_row_lookup(monkeypatch, _PBKDF2_ROW)
|
||||
|
||||
assert blocked.value.code == "429"
|
||||
assert pbkdf2_on_event_loop == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("fips_openssl")
|
||||
async def test_fips_burst_of_wrong_passwords_stops_deriving_once_its_user_is_blocked(
|
||||
monkeypatch: pytest.MonkeyPatch, pbkdf2_on_event_loop: list[bool]
|
||||
) -> None:
|
||||
from litellm.proxy.auth.login_utils import authenticate_user
|
||||
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://stub")
|
||||
repository = _known_user()
|
||||
repository.return_value.table.find_first.return_value.password = _PBKDF2_ROW
|
||||
throttle = _throttle(user_limit=1)
|
||||
with patch( # test-quality-ok: the user lookup is the database boundary
|
||||
"litellm.proxy.auth.login_utils.UserRepository", repository
|
||||
):
|
||||
refusals = await asyncio.gather(
|
||||
*(
|
||||
authenticate_user(
|
||||
username="known@example.com",
|
||||
password="Wrong-Lamp-4-River",
|
||||
master_key="sk-master",
|
||||
prisma_client=MagicMock(),
|
||||
throttle=throttle,
|
||||
general_settings=_POLICY_NO_BREACH_CHECK,
|
||||
)
|
||||
for _ in range(6)
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
assert [refusal.code for refusal in refusals] == ["401", "401", "429", "429", "429", "429"]
|
||||
assert pbkdf2_on_event_loop == [False, False]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue