fix(key-management): block personal key budget delegation from CLI session tokens

When team_table is None (personal key, no team_id in request), the personal key
has no team-budget enforcement at request time. A session token therefore cannot
delegate any explicit max_budget for a personal key -- that would open a budget
bypass path. Block the request with a clear 400 directing the caller to use a
team_id instead.
This commit is contained in:
Sameer Kankute 2026-06-23 18:14:59 +05:30
parent 5be0cc9ce6
commit 434072dfaa
No known key found for this signature in database
2 changed files with 59 additions and 1 deletions

View file

@ -747,7 +747,24 @@ async def _common_key_generation_helper(
)
# Session tokens (lite login) carry max_budget=None to avoid a per-session
# LLM spend cap, but that None must not be read as "unlimited delegation
# authority". Use the team budget as the effective ceiling instead.
# authority". A personal key (no team) has no team-budget enforcement at
# request time, so a session token cannot delegate any budget for one.
if (
user_api_key_dict.is_session_token
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
and not is_ui_session_team_key
and _requested_max_budget is not None
and team_table is None
):
raise HTTPException(
status_code=400,
detail={
"error": (
f"max_budget ({_requested_max_budget}) cannot be set without "
"specifying team_id when using a CLI session token."
)
},
)
delegation_ceiling = (
user_api_key_dict.max_budget
if user_api_key_dict.max_budget is not None

View file

@ -12636,3 +12636,44 @@ async def test_regular_unlimited_user_delegation_ceiling_not_applied():
team_table=team,
)
assert result is not None
@pytest.mark.asyncio
async def test_cli_session_token_personal_key_with_budget_blocked():
caller = UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="user-1",
team_id="team-1",
is_session_token=True,
)
with pytest.raises(HTTPException) as exc_info:
await _common_key_generation_helper(
data=GenerateKeyRequest(max_budget=1000.0),
user_api_key_dict=caller,
litellm_changed_by=None,
team_table=None, # no team in request = personal key
)
assert exc_info.value.status_code == 400
assert "team_id" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_cli_session_token_personal_key_without_budget_allowed():
caller = UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="user-1",
team_id="team-1",
is_session_token=True,
)
with patch(
"litellm.proxy.management_endpoints.key_management_endpoints.generate_key_helper_fn",
new_callable=AsyncMock,
return_value={"key": "sk-test", "expires": None, "user_id": "user-1"},
):
result = await _common_key_generation_helper(
data=GenerateKeyRequest(max_budget=None),
user_api_key_dict=caller,
litellm_changed_by=None,
team_table=None, # no team in request = personal key, but no explicit budget
)
assert result is not None