From d285832022d1f419a583172531ebfad827d5dea5 Mon Sep 17 00:00:00 2001 From: Steeve-Crypto Date: Tue, 13 Oct 2026 10:36:26 -0400 Subject: [PATCH 1/2] fix(proxy): allow authenticated users to call /health/license The dashboard polls GET /health/license on every page load for all roles, including internal_user_viewer. That route was not on the info_routes allowlist, so non-admin sessions hit an admin-only denial and flooded proxy logs with ERROR stacktraces. /health/license only returns non-sensitive license metadata (type, expiry, feature/seat limits) and never exposes the key. Add it next to the other /health info routes so authenticated non-admins can call it. Fixes #46198 --- litellm/proxy/_types.py | 1 + tests/unit/proxy/auth/test_info_routes.py | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index f009390576a..b9d278b93f1 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -692,6 +692,7 @@ class LiteLLMRoutes(enum.Enum): "/model_group/info", "/health", "/health/services", + "/health/license", "/key/list", "/user/filter/ui", "/models", diff --git a/tests/unit/proxy/auth/test_info_routes.py b/tests/unit/proxy/auth/test_info_routes.py index 416924b5f76..0c8f197399e 100644 --- a/tests/unit/proxy/auth/test_info_routes.py +++ b/tests/unit/proxy/auth/test_info_routes.py @@ -149,3 +149,36 @@ def test_v2_user_info_route_access(): valid_token=valid_token, request_data={}, ) + + +def test_health_license_route_in_info_routes(): + """Non-admins hit /health/license from the dashboard sidebar; it must be an info route. + + Regression for https://github.com/BerriAI/litellm/issues/46198 — without this, + internal_user_viewer page loads raise admin-only denials that flood ERROR logs. + """ + assert "/health/license" in LiteLLMRoutes.info_routes.value + assert RouteChecks.is_info_route("/health/license") is True + + +def test_internal_user_viewer_allowed_health_license(): + """internal_user_viewer must be able to call GET /health/license without admin denial.""" + user_obj = LiteLLM_UserTable( + user_id="viewer_user", + user_email="viewer@example.com", + user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, + ) + valid_token = UserAPIKeyAuth(user_id="viewer_user") + request = MagicMock(spec=Request) + request.query_params = {} + + # Must not raise — license metadata is non-sensitive and the dashboard polls it. + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, + route="/health/license", + request=request, + valid_token=valid_token, + request_data={}, + ) + From 0e84562d2dbf2a4eae4094418f9406ca02d2de24 Mon Sep 17 00:00:00 2001 From: Haquant <76264891+Steeve-Crypto@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:18:06 -0400 Subject: [PATCH 2/2] test: assert the viewer route check result --- tests/unit/proxy/auth/test_info_routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/proxy/auth/test_info_routes.py b/tests/unit/proxy/auth/test_info_routes.py index 0c8f197399e..a96e0009b4e 100644 --- a/tests/unit/proxy/auth/test_info_routes.py +++ b/tests/unit/proxy/auth/test_info_routes.py @@ -173,7 +173,7 @@ def test_internal_user_viewer_allowed_health_license(): request.query_params = {} # Must not raise — license metadata is non-sensitive and the dashboard polls it. - RouteChecks.non_proxy_admin_allowed_routes_check( + result = RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, route="/health/license", @@ -181,4 +181,5 @@ def test_internal_user_viewer_allowed_health_license(): valid_token=valid_token, request_data={}, ) + assert result is None