mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(proxy): keep /health inside the caller's team for unrestricted non-admin keys
This commit is contained in:
parent
69a45e81cc
commit
8b3faa6ed8
2 changed files with 64 additions and 5 deletions
|
|
@ -878,11 +878,17 @@ def _health_accessible_model_names(
|
|||
|
||||
|
||||
def _caller_may_probe_deployment(
|
||||
deployment: Mapping[str, object], allowed_models: frozenset[str], llm_router: Router | None, team_id: str | None
|
||||
deployment: Mapping[str, object],
|
||||
allowed_models: frozenset[str] | None,
|
||||
llm_router: Router | None,
|
||||
team_id: str | None,
|
||||
caller_is_admin: bool,
|
||||
) -> bool:
|
||||
"""Same deployment visibility rule as routing: another team's deployment is never in scope, team-less callers included."""
|
||||
if not Router._deployment_usable_by_team(deployment, team_id):
|
||||
if not caller_is_admin and not Router._deployment_usable_by_team(deployment, team_id):
|
||||
return False
|
||||
if allowed_models is None:
|
||||
return True
|
||||
if llm_router is None:
|
||||
return deployment.get("model_name") in allowed_models
|
||||
model: Final = dict(deployment)
|
||||
|
|
@ -1120,12 +1126,12 @@ async def health_endpoint(
|
|||
detail={"error": "Model list not initialized"},
|
||||
)
|
||||
allowed_models: Final = _health_accessible_model_names(user_api_key_dict, llm_router)
|
||||
restrict_to_allowed_models: Final = allowed_models is not None
|
||||
restrict_to_allowed_models: Final = not is_admin or allowed_models is not None
|
||||
_llm_model_list: Final = [
|
||||
m
|
||||
for m in copy.deepcopy(llm_model_list)
|
||||
if allowed_models is None
|
||||
or _caller_may_probe_deployment(m, allowed_models, llm_router, user_api_key_dict.team_id)
|
||||
if not restrict_to_allowed_models
|
||||
or _caller_may_probe_deployment(m, allowed_models, llm_router, user_api_key_dict.team_id, is_admin)
|
||||
]
|
||||
targeted_ids: Final = _resolve_targeted_model_ids(_llm_model_list, model, model_id)
|
||||
if restrict_to_allowed_models and targeted_ids is not None and not targeted_ids:
|
||||
|
|
|
|||
|
|
@ -1836,6 +1836,8 @@ async def test_health_endpoint_treats_no_team_all_team_models_as_unrestricted():
|
|||
user_api_key_dict=UserAPIKeyAuth(
|
||||
api_key="hashed-test-key", models=[SpecialModelNames.all_team_models.value], team_id=None
|
||||
),
|
||||
model=None,
|
||||
model_id=None,
|
||||
)
|
||||
|
||||
assert {m["model_name"] for m in captured["model_list"]} == {"bedrock-nova", "gpt-5.4-mini"}
|
||||
|
|
@ -2844,6 +2846,57 @@ async def test_health_endpoint_hides_team_deployments_from_a_key_with_no_team():
|
|||
assert probed == {"id-bedrock"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("team_id", "expected_ids"),
|
||||
[(None, {"id-bedrock"}), ("team-a", {"id-bedrock"}), ("team-b", {"id-bedrock", "id-team-b"})],
|
||||
)
|
||||
async def test_health_endpoint_keeps_an_unrestricted_non_admin_key_to_its_own_team(team_id, expected_ids):
|
||||
"""
|
||||
A key with no model restriction is still bound by routing's team rule:
|
||||
it may probe global deployments and its own team's, never another team's.
|
||||
"""
|
||||
probed = await _live_probed_model_ids(
|
||||
_TEAM_MODEL_LIST,
|
||||
UserAPIKeyAuth(api_key="hashed-test-key", models=[], team_id=team_id),
|
||||
)
|
||||
|
||||
assert probed == expected_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint_lets_a_proxy_admin_probe_every_teams_deployment():
|
||||
probed = await _live_probed_model_ids(
|
||||
_TEAM_MODEL_LIST,
|
||||
UserAPIKeyAuth(api_key="hashed-test-key", models=[], user_role=LitellmUserRoles.PROXY_ADMIN),
|
||||
)
|
||||
|
||||
assert probed == {"id-bedrock", "id-team-b"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint_keeps_an_unrestricted_non_admin_key_to_its_own_team_on_background_cache_path():
|
||||
from fastapi import Response
|
||||
|
||||
from litellm.proxy.health_endpoints._health_endpoints import health_endpoint
|
||||
|
||||
with _proxy_health_globals(
|
||||
_TEAM_MODEL_LIST,
|
||||
_router_for(_TEAM_MODEL_LIST),
|
||||
use_background_health_checks=True,
|
||||
health_check_results=_TEAM_CACHED_RESULTS,
|
||||
):
|
||||
result = await health_endpoint(
|
||||
response=Response(),
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-test-key", models=[], team_id="team-a"),
|
||||
model=None,
|
||||
model_id=None,
|
||||
)
|
||||
|
||||
assert [ep["model_id"] for ep in result["healthy_endpoints"]] == ["id-bedrock"]
|
||||
assert result["healthy_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint_shows_a_teams_own_deployment_by_its_public_name():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue