From b56809b3b161f3e564a1ce84b0db162dab8ec894 Mon Sep 17 00:00:00 2001 From: jesus Date: Thu, 17 Sep 2026 22:29:49 +0000 Subject: [PATCH] fix(proxy): check project write permission before validating against team limits Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../management_endpoints/project_endpoints.py | 20 +++++++-------- .../test_project_endpoints_prisma.py | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py index ee3bae1fba9..ba930f39080 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py @@ -523,16 +523,6 @@ async def new_project( # Validate team exists and get team object with budget team_object = await _validate_team_exists(team_id=data.team_id, prisma_client=prisma_client) - # Validate project limits against team limits - _check_team_project_limits( - team_object=LiteLLM_TeamTable.model_validate(team_object.model_dump()), - data=data, - ) - - # Opt-in (default off): require rpm/tpm for every model added to the project. - if general_settings.get("enforce_project_model_quota", False): - _raise_on_missing_project_model_quota(data, _router_access_group_names(llm_router)) - # Check if user has permission to create projects for this team # only team admins can create projects for their team has_permission = await _check_user_permission_for_project( @@ -550,6 +540,16 @@ async def new_project( }, ) + # Validate project limits against team limits + _check_team_project_limits( + team_object=LiteLLM_TeamTable.model_validate(team_object.model_dump()), + data=data, + ) + + # Opt-in (default off): require rpm/tpm for every model added to the project. + if general_settings.get("enforce_project_model_quota", False): + _raise_on_missing_project_model_quota(data, _router_access_group_names(llm_router)) + # Generate project_id if not provided if data.project_id is None: data.project_id = str(uuid.uuid4()) diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index d1f39fa8df6..f0410805763 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -1225,6 +1225,31 @@ async def test_new_project_flag_on_missing_rpm_tpm_returns_400(monkeypatch): assert "rpm/tpm quota" in str(exc_info.value) +@pytest.mark.asyncio +async def test_new_project_denies_non_admin_before_leaking_team_limits(monkeypatch): + from litellm.proxy._types import LiteLLM_TeamTable + from litellm_enterprise.proxy.management_endpoints import project_endpoints as pe + + team = LiteLLM_TeamTable(team_id="test-team", models=["secret-model"], max_budget=42.0) + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", mock.MagicMock()) + monkeypatch.setattr(litellm.proxy.proxy_server, "premium_user", True) + monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", {}) + monkeypatch.setattr(pe, "_validate_team_exists", mock.AsyncMock(return_value=team)) + + with pytest.raises(ProxyException) as exc_info: + await new_project( + data=NewProjectRequest(team_id="test-team", models=["not-in-team"], max_budget=1000.0), + http_request=Request(scope={"type": "http"}), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, api_key="sk-user", user_id="stranger" + ), + ) + + assert exc_info.value.code == "403" + assert "secret-model" not in str(exc_info.value) + assert "42" not in str(exc_info.value) + + def _project_update_mocks(monkeypatch, stored_metadata: dict) -> mock.MagicMock: existing_row = mock.MagicMock( team_id=None, budget_id=None, object_permission_id=None, metadata=stored_metadata