From fe74cefba41858161acfc2dd8733f19bf1eb6bf5 Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Tue, 11 Aug 2026 18:41:45 +0200 Subject: [PATCH] fix: validate regeneration routes at executor boundary --- .../key_management_endpoints.py | 7 +++- .../test_key_management_endpoints.py | 36 ++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 2a68a7771e0..aa0f30afd26 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -2067,7 +2067,7 @@ async def prepare_key_update_data( data_json: Final[dict] = data.model_dump(exclude_unset=True) if "key_type" in data.model_fields_set: handle_key_type(data, data_json) - if data.key_type == LiteLLMKeyType.DEFAULT and "allowed_routes" not in data.model_fields_set: + if data.key_type == LiteLLMKeyType.DEFAULT and data_json.get("allowed_routes") is None: data_json["allowed_routes"] = [] data_json.pop("key", None) data_json.pop("new_key", None) @@ -4817,6 +4817,11 @@ async def _execute_virtual_key_regeneration( existing_key_row=key_in_db, user_api_key_dict=user_api_key_dict, ) + _check_key_type_transition_against_existing_routes( + data=data, + existing_key_row=key_in_db, + user_api_key_dict=user_api_key_dict, + ) # Apply the same membership rule used on /key/update: when the caller # asks to point the regenerated key at a different organization_id, # require they are a member of (or proxy admin over) the target org. diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index ef34fed014e..ea0806ec107 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -11437,7 +11437,7 @@ async def test_regenerate_rejects_route_transition_before_recording_deletion(): execute_mock, ), ): - with pytest.raises(HTTPException) as exc_info: + with pytest.raises(ProxyException) as exc_info: await regenerate_key_fn( key="sk-old", data=RegenerateKeyRequest(key_type=LiteLLMKeyType.LLM_API), @@ -11447,8 +11447,8 @@ async def test_regenerate_rejects_route_transition_before_recording_deletion(): ), ) - assert exc_info.value.status_code == 403 - assert "allowed_routes" in str(exc_info.value.detail) + assert exc_info.value.code == "403" + assert "allowed_routes" in str(exc_info.value.message) persist_deleted_mock.assert_not_awaited() execute_mock.assert_not_awaited() @@ -12145,7 +12145,30 @@ class TestAllowedRoutesCallerPermission: assert "allowed_routes" not in update_data @pytest.mark.asyncio - async def test_update_key_default_type_clears_existing_preset_routes(self): + @pytest.mark.parametrize( + "data", + [ + pytest.param( + UpdateKeyRequest( + key="sk-test", + key_type=LiteLLMKeyType.DEFAULT, + allowed_routes=None, + ), + id="explicit-null", + ), + pytest.param( + UpdateKeyRequest( + key="sk-test", + key_type=LiteLLMKeyType.DEFAULT, + ), + id="omitted", + ), + ], + ) + async def test_update_key_default_type_clears_existing_preset_routes( + self, + data: UpdateKeyRequest, + ): """An explicit default preset must remove the stored route restriction.""" from types import SimpleNamespace @@ -12154,10 +12177,7 @@ class TestAllowedRoutesCallerPermission: ) update_data = await prepare_key_update_data( - data=UpdateKeyRequest( - key="sk-test", - key_type=LiteLLMKeyType.DEFAULT, - ), + data=data, existing_key_row=SimpleNamespace( metadata={}, team_id=None,