From a363f68c3ce693179720cda74bb0aaed066f1569 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Sun, 8 Nov 2026 15:04:34 +0800 Subject: [PATCH] fix(alerts): honor explicit parent route grants --- litellm/proxy/alerting_endpoints/usage_alerts.py | 2 ++ tests/integration/database/test_usage_alerts.py | 5 +++-- .../proxy/alerting_endpoints/test_usage_alerts.py | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/alerting_endpoints/usage_alerts.py b/litellm/proxy/alerting_endpoints/usage_alerts.py index c954c40cc48..3950efdc1b4 100644 --- a/litellm/proxy/alerting_endpoints/usage_alerts.py +++ b/litellm/proxy/alerting_endpoints/usage_alerts.py @@ -49,6 +49,8 @@ ScopeId: TypeAlias = Annotated[str | None, Query(min_length=1, max_length=1024)] def _allows_write(grant: str, route: str) -> bool: if grant == "info_routes": return False + if route == grant or route.startswith(grant + "/"): + return True paths: Final = ( TypeAdapter(tuple[str, ...]).validate_python(LiteLLMRoutes[grant].value) if grant in LiteLLMRoutes.__members__ diff --git a/tests/integration/database/test_usage_alerts.py b/tests/integration/database/test_usage_alerts.py index 77362be35d0..5339e451e51 100644 --- a/tests/integration/database/test_usage_alerts.py +++ b/tests/integration/database/test_usage_alerts.py @@ -39,12 +39,13 @@ async def test_alert_resource_labels_bound_legacy_display_values(db: ManagementD await repository.database.query_raw('DELETE FROM "LiteLLM_UserTable" WHERE user_id=$1', identity) -def test_scoped_rules_use_real_management_auth_and_preserve_read_only_roles(gateway: Gateway) -> None: +@pytest.mark.parametrize("grant", ["management_routes", "/usage", "/usage/alert-rules"]) +def test_scoped_rules_use_real_management_auth_and_preserve_read_only_roles(gateway: Gateway, grant: str) -> None: with gateway.scenario() as scenario: owner: Final = scenario.user(user_role="internal_user") outsider: Final = scenario.user(user_role="internal_user") viewer: Final = scenario.user(user_role="proxy_admin_viewer") - owner_key: Final = scenario.key(user_id=owner, key_type="management") + owner_key: Final = scenario.key(user_id=owner, key_type="default", allowed_routes=[grant]) outsider_key: Final = scenario.key(user_id=outsider, key_type="management") viewer_key: Final = scenario.key(user_id=viewer, key_type="read_only") inference_key: Final = scenario.key(user_id=owner, key_type="llm_api") diff --git a/tests/unit/proxy/alerting_endpoints/test_usage_alerts.py b/tests/unit/proxy/alerting_endpoints/test_usage_alerts.py index 1270b6809a9..239151f9ce1 100644 --- a/tests/unit/proxy/alerting_endpoints/test_usage_alerts.py +++ b/tests/unit/proxy/alerting_endpoints/test_usage_alerts.py @@ -6,7 +6,7 @@ import pytest from fastapi import HTTPException from litellm.proxy._types import UserAPIKeyAuth -from litellm.proxy.alerting_endpoints.usage_alerts import require_scope, require_write_access +from litellm.proxy.alerting_endpoints.usage_alerts import _allows_write, require_scope, require_write_access from litellm.repositories.usage_alerts import UsageAlertsRepository, activity_path from litellm.types.proxy.auth.user_roles import LitellmUserRoles @@ -25,6 +25,19 @@ def test_info_grants_do_not_authorize_mutations(role: LitellmUserRoles, key_type assert error.value.status_code == 403 +@pytest.mark.parametrize("grant", ["management_routes", "/usage", "/usage/alert-rules", "/usage/*"]) +def test_explicit_route_grants_authorize_alert_mutations(grant: str) -> None: + assert _allows_write(grant, "/usage/alert-rules/example") is True + + +@pytest.mark.parametrize("grant", ["/usag", "/usage/alert-rule", "/usage/alerts"]) +def test_route_grants_require_a_complete_path_segment(grant: str) -> None: + user: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, allowed_routes=[grant]) + with pytest.raises(HTTPException) as error: + require_write_access(user, "/usage/alert-rules/example") + assert error.value.status_code == 403 + + @pytest.mark.asyncio @pytest.mark.parametrize( "role,key_type,write",