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>
This commit is contained in:
jesus 2026-09-17 22:29:49 +00:00
parent 7c0e586d78
commit b56809b3b1
2 changed files with 35 additions and 10 deletions

View file

@ -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())

View file

@ -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