mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(jwt): make AUTO_REGISTER functional in prod; raise on missing winner
Two correctness fixes flagged by Greptile on the AUTO_REGISTER path: 1. generate_key_helper_fn was called without table_name="key". Without that, the helper falls into the user-upsert branch (table_name in (None, "user")) and tries to insert into LiteLLM_UserTable with user_id=None, which hits the NOT NULL @id constraint. AUTO_REGISTER would never have succeeded in production. Now passes table_name="key" explicitly, matching the /key/generate caller. 2. When the race loser refetches the winner's mapping and gets None (winner row concurrently deleted), the previous code returned None — and the caller in _resolve_jwt_to_virtual_key then fell through to less- restrictive team-based JWT auth, silently bypassing the configured AUTO_REGISTER policy. Now raises HTTP 503 so the caller retries against a stable state rather than getting unintended fallback access. Adds one test for the 503 winner-vanishes path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c42c5bd4b
commit
437f316a27
2 changed files with 71 additions and 2 deletions
|
|
@ -650,8 +650,14 @@ async def _auto_register_jwt_mapping(
|
|||
generate_key_helper_fn,
|
||||
)
|
||||
|
||||
# ``table_name="key"`` is required: without it, generate_key_helper_fn
|
||||
# falls into the user-upsert branch (`table_name is None or "user"`) and
|
||||
# attempts to insert into LiteLLM_UserTable with user_id=None, which fails
|
||||
# the NOT NULL @id constraint. Every successful key-creation caller (e.g.
|
||||
# /key/generate) passes table_name="key" explicitly.
|
||||
key_data = await generate_key_helper_fn(
|
||||
request_type="key",
|
||||
table_name="key",
|
||||
team_id=team_id,
|
||||
metadata={
|
||||
"auto_registered": True,
|
||||
|
|
@ -705,8 +711,18 @@ async def _auto_register_jwt_mapping(
|
|||
prisma_client=prisma_client,
|
||||
)
|
||||
if token_hash is None:
|
||||
# Should not happen, but guard against a delete racing our fetch.
|
||||
return None
|
||||
# The winner's mapping vanished between the unique-constraint
|
||||
# conflict and our re-fetch (concurrent delete). Returning None
|
||||
# here would silently fall through to team-based JWT auth —
|
||||
# a less-restrictive path than the operator configured. Raise
|
||||
# 503 so the caller retries against a stable state instead.
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=(
|
||||
"JWT Key Mapping: AUTO_REGISTER race resolution failed — "
|
||||
"winner's mapping was concurrently removed. Retry the request."
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -1069,6 +1069,59 @@ async def test_auto_register_race_conflict_tolerates_delete_failure():
|
|||
prisma_client.db.litellm_verificationtoken.delete.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_register_raises_503_when_winner_mapping_vanishes():
|
||||
"""
|
||||
Race edge case: this request loses the unique-constraint race, deletes its
|
||||
orphan, then refetches the winner's mapping — but the winner's row was
|
||||
concurrently deleted. Previously this returned None, silently falling
|
||||
through to less-restrictive team-based JWT auth (bypassing the configured
|
||||
AUTO_REGISTER policy). Must now raise HTTP 503 so the caller retries
|
||||
rather than getting unintended fallback access.
|
||||
"""
|
||||
from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping
|
||||
from litellm.proxy._types import UnregisteredJWTClientBehavior
|
||||
|
||||
jwt_handler = JWTHandler()
|
||||
jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth(
|
||||
virtual_key_claim_field="sub",
|
||||
unregistered_jwt_client_behavior=UnregisteredJWTClientBehavior.AUTO_REGISTER,
|
||||
virtual_key_mapping_cache_ttl=300,
|
||||
)
|
||||
|
||||
prisma_client = MagicMock()
|
||||
prisma_client.db.litellm_jwtkeymapping.create = AsyncMock(
|
||||
side_effect=Exception("Unique constraint failed (P2002)")
|
||||
)
|
||||
prisma_client.db.litellm_verificationtoken.delete = AsyncMock()
|
||||
# Winner row no longer exists by the time we refetch
|
||||
prisma_client.db.litellm_jwtkeymapping.find_first = AsyncMock(return_value=None)
|
||||
|
||||
user_api_key_cache = DualCache()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.key_management_endpoints.generate_key_helper_fn",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"token": "sk-loser", "key": "sk-loser"},
|
||||
),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
await _auto_register_jwt_mapping(
|
||||
virtual_key_claim_field="sub",
|
||||
claim_value="user-42",
|
||||
jwt_handler=jwt_handler,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
parent_otel_span=None,
|
||||
proxy_logging_obj=None,
|
||||
cache_key="jwt_key_mapping:sub:user-42",
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 503
|
||||
assert "concurrently removed" in exc_info.value.detail
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Tests: AUTO_REGISTER inherits team_id from JWT
|
||||
# ──────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue