mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(router): apply the team guard to exact deployment-id lookups
A caller who knew another team's deployment id could resolve its credentials and model through the exact-id branch of _resolve_unblocked_deployment, which skipped the team filter that the name and wildcard branches already apply. The exact-id hit now goes through _deployment_usable_by_team as well, so no lookup path resolves a deployment owned by a different team The router's internal _acancel_batch re-resolves credentials by the deployment id that async_get_available_deployment already picked and team-authorized, so it re-resolves with that deployment's own team id to stay compatible with the guard
This commit is contained in:
parent
adbb3e1bce
commit
76ae0a8e4b
4 changed files with 32 additions and 13 deletions
|
|
@ -5378,10 +5378,15 @@ class Router:
|
|||
request_kwargs=kwargs,
|
||||
)
|
||||
|
||||
selected_deployment_id = (deployment.get("model_info") or {}).get("id")
|
||||
selected_model_info = deployment.get("model_info") or {}
|
||||
selected_deployment_id = selected_model_info.get("id")
|
||||
data = deployment["litellm_params"].copy()
|
||||
# async_get_available_deployment already team-authorized this
|
||||
# deployment; re-resolve with its owner team so the resolver's
|
||||
# team guard doesn't reject the deployment it was handed.
|
||||
resolved_credentials = self.get_deployment_credentials_with_provider(
|
||||
model_id=selected_deployment_id or model
|
||||
model_id=selected_deployment_id or model,
|
||||
team_id=selected_model_info.get("team_id"),
|
||||
)
|
||||
if resolved_credentials is not None:
|
||||
data.update(resolved_credentials)
|
||||
|
|
@ -8647,12 +8652,16 @@ class Router:
|
|||
|
||||
Both the credential resolver and the alias-to-model resolver delegate
|
||||
here so a mixed model group can never hand one caller the credentials
|
||||
of one deployment and the model of another. Name and wildcard lookups
|
||||
skip deployments owned by a team other than ``team_id``; passing
|
||||
``team_id`` also unlocks that team's own deployments (exact team public
|
||||
model name and team wildcard patterns).
|
||||
of one deployment and the model of another. Every lookup path -
|
||||
exact deployment id, model-group name, and wildcard - skips
|
||||
deployments owned by a team other than ``team_id``, so a caller who
|
||||
knows another team's deployment id cannot resolve its credentials or
|
||||
model; passing ``team_id`` also unlocks that team's own deployments
|
||||
(exact team public model name and team wildcard patterns).
|
||||
"""
|
||||
deployment = self.get_deployment(model_id=model_id)
|
||||
if deployment is not None and not self._deployment_usable_by_team(deployment, team_id):
|
||||
deployment = None
|
||||
|
||||
if deployment is None:
|
||||
deployment = self._get_model_group_deployment_usable_by_team(model_group_name=model_id, team_id=team_id)
|
||||
|
|
@ -8775,10 +8784,11 @@ class Router:
|
|||
model_id: Model ID or model name from model_list (e.g., "gpt-4o-litellm")
|
||||
team_id: Optional team id of the caller. When set, team-scoped
|
||||
deployments (indexed by team public model name, including team
|
||||
wildcard models like "openai/*") are also considered. Name and
|
||||
wildcard lookups never resolve a deployment owned by a
|
||||
different team, so shared model names can't leak another
|
||||
team's credentials.
|
||||
wildcard models like "openai/*") are also considered. No lookup
|
||||
path - exact deployment id, model-group name, or wildcard -
|
||||
ever resolves a deployment owned by a different team, so
|
||||
neither shared model names nor known deployment ids can leak
|
||||
another team's credentials.
|
||||
|
||||
Returns:
|
||||
Dictionary containing api_key, api_base, custom_llm_provider, etc.
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ async def test_vector_store_file_list_resolves_credentials_from_model_query_para
|
|||
assert result["model"] == "openai/gpt-4o-mini"
|
||||
assert "custom_llm_provider" not in result
|
||||
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
|
||||
model_id="team-openai"
|
||||
model_id="team-openai", team_id=None
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ async def test_vector_store_file_list_resolves_managed_vector_store_before_team_
|
|||
|
||||
llm_router = MagicMock()
|
||||
|
||||
def get_credentials(model_id):
|
||||
def get_credentials(model_id, team_id=None):
|
||||
return {
|
||||
"api_key": f"sk-{model_id}",
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
|
|
@ -171,7 +171,7 @@ async def test_vector_store_file_list_resolves_managed_vector_store_before_team_
|
|||
assert captured_data["api_key"] == "sk-managed-deployment"
|
||||
assert captured_data["model"] == "openai/managed-deployment"
|
||||
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
|
||||
model_id="managed-deployment"
|
||||
model_id="managed-deployment", team_id=None
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5892,6 +5892,15 @@ def test_get_deployment_model_for_alias_matches_credential_deployment_per_team()
|
|||
f"team_id={team_id}: credentials came from a different deployment than the model"
|
||||
)
|
||||
|
||||
# A caller who knows another team's exact deployment id must not resolve
|
||||
# its model or credentials through it either.
|
||||
for outsider_team_id in [None, "team-b"]:
|
||||
assert router.get_deployment_model_for_alias(model_id="team-a-dep", team_id=outsider_team_id) is None
|
||||
assert router.get_deployment_credentials_with_provider(model_id="team-a-dep", team_id=outsider_team_id) is None
|
||||
assert router.get_deployment_model_for_alias(model_id="team-a-dep", team_id="team-a") == (
|
||||
"bedrock/team-a-private-model"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_unblocked_deployment_resolves_alias_id_and_wildcard():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue