diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2c97647979..ed20fe86cd 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -656,6 +656,13 @@ class LiteLLMRoutes(enum.Enum): "/health/services", ] + info_routes + # Stateless validators on caller-supplied log data; source logs are + # already accessible via spend_tracking_routes, so no scope expansion. + compliance_check_routes = [ + "/compliance/eu-ai-act", + "/compliance/gdpr", + ] + # Routes in `global_spend_tracking_routes` return proxy-wide spend across # every team, customer, and api_key. They are intentionally NOT included # here — non-admin roles must not see other tenants' spend. Admin roles go @@ -675,6 +682,7 @@ class LiteLLMRoutes(enum.Enum): ] + spend_tracking_routes + key_management_routes + + compliance_check_routes ) internal_user_view_only_routes = spend_tracking_routes diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index cf6feabf85..268cdff1f7 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -53,6 +53,61 @@ def test_non_admin_config_update_route_rejected(): assert "Your role=internal_user" in str(exc_info.value) +@pytest.mark.parametrize( + "route", + ["/compliance/eu-ai-act", "/compliance/gdpr"], +) +def test_compliance_routes_open_to_internal_user(route): + """Compliance routes are stateless validators on caller-supplied log data + - non-admin internal_user roles can call them.""" + role = LitellmUserRoles.INTERNAL_USER.value + user_obj = LiteLLM_UserTable( + user_id="test_user", + user_email="test@example.com", + user_role=role, + ) + valid_token = UserAPIKeyAuth(user_id="test_user", user_role=role) + request = MagicMock(spec=Request) + request.query_params = {} + + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=role, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + + +@pytest.mark.parametrize( + "route", + ["/compliance/eu-ai-act", "/compliance/gdpr"], +) +def test_compliance_routes_blocked_for_internal_user_view_only(route): + """Deprecated internal_user_viewer role must not gain compliance route access.""" + role = LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value + user_obj = LiteLLM_UserTable( + user_id="test_user", + user_email="test@example.com", + user_role=role, + ) + valid_token = UserAPIKeyAuth(user_id="test_user", user_role=role) + request = MagicMock(spec=Request) + request.query_params = {} + + with pytest.raises(Exception) as exc_info: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=role, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + assert "Only proxy admin can be used" in str(exc_info.value) + + def test_proxy_admin_viewer_config_update_route_rejected(): """Test that proxy admin viewer users are rejected when trying to call /config/update"""