mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(auth): enforce JWT end-user budget rate limits (#51405)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
40981a5d89
commit
00bd56760d
2 changed files with 79 additions and 2 deletions
|
|
@ -4008,7 +4008,11 @@ async def run_centralized_common_checks(
|
|||
user_api_key_auth_obj.project_metadata = project_object.metadata
|
||||
user_api_key_auth_obj.project_alias = project_object.project_alias
|
||||
|
||||
if end_user_id and key_end_user_budget_id is not None and prisma_client is not None:
|
||||
if (
|
||||
end_user_id
|
||||
and (key_end_user_budget_id is not None or user_api_key_auth_obj.jwt_claims is not None)
|
||||
and prisma_client is not None
|
||||
):
|
||||
await _apply_key_end_user_default_budget_to_token(
|
||||
valid_token=user_api_key_auth_obj,
|
||||
end_user_object=end_user_object,
|
||||
|
|
@ -4196,7 +4200,7 @@ async def _noop_none() -> None:
|
|||
async def _apply_key_end_user_default_budget_to_token(
|
||||
valid_token: UserAPIKeyAuth,
|
||||
end_user_object: LiteLLM_EndUserTable | None,
|
||||
key_end_user_budget_id: str,
|
||||
key_end_user_budget_id: str | None,
|
||||
prisma_client: PrismaClient,
|
||||
user_api_key_cache: UserApiKeyCache,
|
||||
parent_otel_span: Span | None,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|||
|
||||
|
||||
import fastapi
|
||||
import jwt
|
||||
import orjson
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from starlette.routing import Route
|
||||
|
||||
from litellm.redis.stores.connection import connect
|
||||
|
|
@ -39,6 +41,7 @@ from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
|||
from litellm.proxy.common_utils.user_api_key_cache import (
|
||||
NO_TEAM_MEMBERSHIP_SENTINEL,
|
||||
UserApiKeyCache,
|
||||
end_user_cache_key,
|
||||
team_membership_reservation_cache_key,
|
||||
)
|
||||
from litellm.proxy.hooks.model_max_budget_limiter import (
|
||||
|
|
@ -1998,6 +2001,76 @@ def test_proxy_admin_jwt_auth_handles_no_team_object():
|
|||
assert result.end_user_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("source", ["claim", "body"])
|
||||
@pytest.mark.parametrize("limits", [(1, 7), (0, 0), (None, None)])
|
||||
async def test_jwt_customer_budget_rate_limits_reach_enforcement(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
source: str,
|
||||
limits: tuple[int | None, int | None],
|
||||
) -> None:
|
||||
cache: Final = UserApiKeyCache()
|
||||
database: Final = MagicMock()
|
||||
handler: Final = JWTHandler()
|
||||
handler.update_environment(
|
||||
prisma_client=database,
|
||||
user_api_key_cache=cache,
|
||||
litellm_jwtauth=LiteLLM_JWTAuth(
|
||||
team_id_jwt_field="team_id",
|
||||
end_user_id_jwt_field="customer_id" if source == "claim" else None,
|
||||
),
|
||||
)
|
||||
private_key: Final = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
jwks_url: Final = "https://identity.invalid/rate-limits/jwks"
|
||||
cache.set_cache(
|
||||
f"litellm_jwt_auth_keys_{jwks_url}",
|
||||
[json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(private_key.public_key()))],
|
||||
)
|
||||
cache.set_cache(
|
||||
"team_id:rate-limit-team",
|
||||
LiteLLM_TeamTable(team_id="rate-limit-team", models=[]),
|
||||
model_type=LiteLLM_TeamTable,
|
||||
)
|
||||
cache.set_cache(
|
||||
end_user_cache_key("rate-limit-customer"),
|
||||
LiteLLM_EndUserTable(
|
||||
user_id="rate-limit-customer",
|
||||
blocked=False,
|
||||
litellm_budget_table=LiteLLM_BudgetTable(rpm_limit=limits[0], tpm_limit=limits[1]),
|
||||
),
|
||||
model_type=LiteLLM_EndUserTable,
|
||||
)
|
||||
monkeypatch.setenv("JWT_PUBLIC_KEY_URL", jwks_url)
|
||||
monkeypatch.delenv("JWT_ISSUER", raising=False)
|
||||
monkeypatch.delenv("JWT_AUDIENCE", raising=False)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "jwt_handler", handler)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", database)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "user_api_key_cache", cache)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "premium_user", True)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "master_key", "rate-limit-master")
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "user_custom_auth", None)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", None)
|
||||
monkeypatch.setattr(
|
||||
litellm.proxy.proxy_server, "general_settings", {"enable_jwt_auth": True, "disable_budget_reservation": True}
|
||||
)
|
||||
token: Final = jwt.encode(
|
||||
{"team_id": "rate-limit-team", **({"customer_id": "rate-limit-customer"} if source == "claim" else {})},
|
||||
private_key,
|
||||
algorithm="RS256",
|
||||
)
|
||||
request: Final = Request(
|
||||
{"type": "http", "method": "POST", "path": "/chat/completions", "headers": [], "query_string": b""}
|
||||
)
|
||||
request._body = json.dumps(
|
||||
{"model": "rate-limit-model", **({"user": "rate-limit-customer"} if source == "body" else {})}
|
||||
).encode()
|
||||
try:
|
||||
auth: Final = await user_api_key_auth(request=request, api_key=f"Bearer {token}")
|
||||
assert (auth.end_user_id, auth.end_user_rpm_limit, auth.end_user_tpm_limit) == ("rate-limit-customer", *limits)
|
||||
finally:
|
||||
await handler.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_standard_jwt_auth_propagates_user_email():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue