diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 5122c64ea64..8d49020461d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -512,6 +512,7 @@ class LiteLLMRoutes(enum.Enum): KeyManagementRoutes.KEY_UNBLOCK.value, KeyManagementRoutes.KEY_BULK_UPDATE.value, KeyManagementRoutes.TEAM_DAILY_ACTIVITY.value, + KeyManagementRoutes.KEY_RESET_SPEND.value, ] management_routes = [ diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index ec1a13b8abc..f1e96f3e660 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1116,3 +1116,77 @@ def test_route_in_additional_public_routes_exact_match(): assert route_in_additonal_public_routes("/status") is True # Non-matching routes should fail assert route_in_additonal_public_routes("/other") is False + + +def test_internal_user_can_access_key_reset_spend_route(): + """ + Regression test: team admins (role=internal_user) should pass the route-level + check for /key/{hash}/reset_spend. The endpoint itself enforces team admin status. + """ + user_obj = LiteLLM_UserTable( + user_id="team-admin-user", + user_email="teamadmin@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + valid_token = UserAPIKeyAuth( + user_id="team-admin-user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + key_hash = "baec26d2901589fe9fec76610e6e2be4895cdd8e19b3ada9a4fa2eb85e1901ae" + route = f"/key/{key_hash}/reset_spend" + + # Should not raise — the route-level check must pass for team admins + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + + +def test_non_admin_non_team_admin_cannot_access_config_update_but_can_attempt_reset_spend(): + """ + An internal_user passes the route check for /key/{hash}/reset_spend + (authorization is deferred to the endpoint), but is still blocked from + admin-only routes like /config/update. + """ + user_obj = LiteLLM_UserTable( + user_id="regular-user", + user_email="user@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + valid_token = UserAPIKeyAuth( + user_id="regular-user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + key_hash = "baec26d2901589fe9fec76610e6e2be4895cdd8e19b3ada9a4fa2eb85e1901ae" + + # /key/{hash}/reset_spend passes the route check for internal_user + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route=f"/key/{key_hash}/reset_spend", + request=request, + valid_token=valid_token, + request_data={}, + ) + + # /config/update is still blocked + with pytest.raises(Exception) as exc_info: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route="/config/update", + request=request, + valid_token=valid_token, + request_data={}, + ) + assert "Only proxy admin can be used to generate" in str(exc_info.value)