From 0070d1b437289e6e7ea7a237d7b144a2552858e3 Mon Sep 17 00:00:00 2001 From: shivam Date: Sat, 18 Jul 2026 21:55:16 +0000 Subject: [PATCH] fix(auth): allow non-admin virtual keys to call /guardrails/apply_guardrail is_llm_api_route did not check apply_guardrail_routes, so a normal virtual key hit the admin-only branch in non_proxy_admin_allowed_routes_check even though the routes are part of llm_api_routes. Also register the /apply_guardrail alias in apply_guardrail_routes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/_types.py | 1 + litellm/proxy/auth/route_checks.py | 3 + .../proxy/auth/test_route_checks.py | 59 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b47b43411c5..9b2201b9313 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -510,6 +510,7 @@ class LiteLLMRoutes(enum.Enum): apply_guardrail_routes = [ "/guardrails/apply_guardrail", + "/apply_guardrail", ] llm_api_routes = ( diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index dd0a34a7898..3c4007e4a8b 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -375,6 +375,9 @@ class RouteChecks: if route in LiteLLMRoutes.litellm_native_routes.value: return True + if route in LiteLLMRoutes.apply_guardrail_routes.value: + return True + # fuzzy match routes like "/v1/threads/thread_49EIN5QF32s4mH20M7GFKdlZ" # Check for routes with placeholders or wildcard patterns for openai_route in LiteLLMRoutes.openai_routes.value: diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index a6d4dc63697..9b45dc00564 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -528,6 +528,65 @@ def test_realtime_webrtc_http_routes_classified_as_llm_api(route): assert RouteChecks.is_management_route(route=route) is False +@pytest.mark.parametrize( + "route", + [ + "/guardrails/apply_guardrail", + "/apply_guardrail", + ], +) +def test_apply_guardrail_routes_classified_as_llm_api(route): + """apply_guardrail routes must be classified as LLM API routes so non-admin + virtual keys can call them instead of hitting the admin-only 401 branch in + non_proxy_admin_allowed_routes_check. + + Regression test for LIT-4579: the routes lived in llm_api_routes (via + apply_guardrail_routes) but is_llm_api_route did not check them, so a normal + key got a 403/401 unless the exact path was listed in allowed_routes. + """ + + assert RouteChecks.is_llm_api_route(route=route) is True + + +@pytest.mark.parametrize( + "route", + [ + "/guardrails/apply_guardrail", + "/apply_guardrail", + ], +) +@pytest.mark.parametrize("allowed_routes", [None, ["llm_api_routes"]]) +def test_non_admin_key_allowed_apply_guardrail_without_explicit_allowlist(route, allowed_routes): + """Regression test for LIT-4579. + + A non-admin virtual key with default LLM API access (allowed_routes=None) or + the llm_api_routes preset must be able to call apply_guardrail without adding + the exact path to allowed_routes. Before the fix this raised the admin-only + exception because is_llm_api_route ignored apply_guardrail_routes. + """ + user_obj = LiteLLM_UserTable( + user_id="test_user", + user_email="test@example.com", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + valid_token = UserAPIKeyAuth( + user_id="test_user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + allowed_routes=allowed_routes, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + 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_virtual_key_allowed_routes_with_litellm_routes_member_name_denied(): """Test that virtual key is denied when route is not in the allowed LiteLLMRoutes group"""