diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 7460522f53e..f3b43abc5a0 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -1234,6 +1234,17 @@ async def patch_model( param=None, ) + # Pause/resume (`blocked`) is proxy-admin only. A non-admin team member + # otherwise goes through member auto-router authorization, which refuses + # a blocked-only patch as an auto-router write. Check the flag first. + if patch_data.blocked is not None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: + raise ProxyException( + message="Only proxy admins can change a model's blocked flag.", + type=ProxyErrorTypes.auth_error.value, + code=status.HTTP_403_FORBIDDEN, + param="blocked", + ) + write_authorization: Final = await ModelManagementAuthChecks.can_user_make_model_call( model_params=db_model, user_api_key_dict=user_api_key_dict, @@ -1270,17 +1281,6 @@ async def patch_model( else patch_data ) - # Pause/resume (`blocked`) is a proxy-admin-only privilege. Team admins - # passed the auth check above for team-scoped models, but they must not - # be able to unblock (or block) a model their proxy admin has paused. - if patch_data.blocked is not None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: - raise ProxyException( - message="Only proxy admins can change a model's blocked flag.", - type=ProxyErrorTypes.auth_error.value, - code=status.HTTP_403_FORBIDDEN, - param="blocked", - ) - ModelManagementAuthChecks.can_user_attach_credential( litellm_params=patch_data.litellm_params, user_api_key_dict=user_api_key_dict, diff --git a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py index ad75344afba..3239a5730ac 100644 --- a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py @@ -4949,6 +4949,60 @@ class TestPatchModelBlockedAuthGate: assert getattr(err, "param", "") == "blocked" assert "proxy admin" in getattr(err, "message", "").lower() + @pytest.mark.asyncio + @pytest.mark.parametrize("role", [LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.ORG_ADMIN]) + async def test_non_admin_blocked_only_patch_is_not_an_auto_router_refusal(self, role: LitellmUserRoles): + """A member who sends only `blocked` must get the blocked-flag refusal. + + Member auto-router authorization runs for non-admins and would answer + "This team does not allow you to manage your own auto routers." The + blocked check has to win before that path is entered. + """ + from litellm.proxy.management_endpoints.model_management_endpoints import ( + patch_model, + ) + + member = UserAPIKeyAuth(user_id="member", user_role=role) + existing_row = MagicMock() + existing_row.litellm_params = {"model": "openai/gpt-4o-mini"} + existing_row.model_dump.return_value = { + "model_name": "gpt-4o-mini", + "litellm_params": existing_row.litellm_params, + "model_info": {"id": "m1", "team_id": "team-1"}, + } + existing_row.model_dump_json.return_value = "{}" + team = LiteLLM_TeamTable( + team_id="team-1", + members_with_roles=[Member(user_id="member", role="user")], + team_member_permissions=[], + ) + team_row = MagicMock() + team_row.model_dump.return_value = team.model_dump() + + mock_prisma = MagicMock() + mock_prisma.db.litellm_proxymodeltable.find_unique = AsyncMock(return_value=existing_row) + mock_prisma.db.litellm_proxymodeltable.update = AsyncMock() + mock_prisma.db.litellm_teamtable.find_unique = AsyncMock(return_value=team_row) + + with ( + patch("litellm.proxy.proxy_server.prisma_client", mock_prisma), + patch("litellm.proxy.proxy_server.llm_router", MagicMock(**{"get_model_ids.return_value": ["m1"]})), + patch("litellm.proxy.proxy_server.store_model_in_db", True), + patch("litellm.proxy.proxy_server.premium_user", True), + ): + with pytest.raises( + ProxyException, match="Only proxy admins can change a model's blocked flag\\." + ) as exc_info: + await patch_model( + model_id="m1", + patch_data=updateDeployment(blocked=True), + user_api_key_dict=member, + ) + + assert exc_info.value.code == "403" + assert exc_info.value.param == "blocked" + mock_prisma.db.litellm_proxymodeltable.update.assert_not_awaited() + @pytest.mark.asyncio async def test_proxy_admin_can_toggle_blocked(self): from litellm.proxy.management_endpoints.model_management_endpoints import ( diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 7cc7415d52d..14e22fe7703 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -1598,9 +1598,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1617,9 +1614,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1636,9 +1630,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1655,9 +1646,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1674,9 +1662,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1693,9 +1678,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1712,9 +1694,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1731,9 +1710,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1750,9 +1726,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1775,9 +1748,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1800,9 +1770,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1825,9 +1792,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1850,9 +1814,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1875,9 +1836,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1900,9 +1858,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1925,9 +1880,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2208,9 +2160,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2227,9 +2176,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2246,9 +2192,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2265,9 +2208,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [