[UI QA checklist] (#9957)

* fix typo on UI

* fix for edit user tab

* fix for user spend

* add /team/permissions_list to management routes

* fix auth check for team member permissions

* fix team endpoints test
This commit is contained in:
Ishaan Jaff 2025-04-12 20:41:50 -07:00 committed by GitHub
parent 2ed63da5f8
commit 89dfb42697
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 155 additions and 74 deletions

View file

@ -371,6 +371,8 @@ class LiteLLMRoutes(enum.Enum):
"/team/block",
"/team/unblock",
"/team/available",
"/team/permissions_list",
"/team/permissions_update",
# model
"/model/new",
"/model/update",
@ -456,6 +458,8 @@ class LiteLLMRoutes(enum.Enum):
self_managed_routes = [
"/team/member_add",
"/team/member_delete",
"/team/permissions_list",
"/team/permissions_update",
"/model/new",
"/model/update",
"/model/delete",

View file

@ -1952,30 +1952,62 @@ async def team_member_permissions(
"""
Get the team member permissions for a team
"""
from litellm.proxy.proxy_server import prisma_client
from litellm.proxy.proxy_server import (
prisma_client,
proxy_logging_obj,
user_api_key_cache,
)
if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"})
team_row = await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
existing_team_row = await get_team_object(
team_id=team_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=None,
proxy_logging_obj=proxy_logging_obj,
check_cache_only=False,
check_db_only=True,
)
if team_row is None:
if existing_team_row is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={team_id}"},
detail={"error": f"Team not found for team_id={team_id}"},
)
team_obj = LiteLLM_TeamTable(**team_row.model_dump())
if team_obj.team_member_permissions is None:
team_obj.team_member_permissions = (
complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
if (
hasattr(user_api_key_dict, "user_role")
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
and not _is_user_team_admin(
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
)
and not _is_available_team(
team_id=complete_team_data.team_id,
user_api_key_dict=user_api_key_dict,
)
):
raise HTTPException(
status_code=403,
detail={
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
"/team/member_add",
complete_team_data.team_id,
)
},
)
if existing_team_row.team_member_permissions is None:
existing_team_row.team_member_permissions = (
TeamMemberPermissionChecks.default_team_member_permissions()
)
return GetTeamMemberPermissionsResponse(
team_id=team_id,
team_member_permissions=team_obj.team_member_permissions,
team_member_permissions=existing_team_row.team_member_permissions,
all_available_permissions=TeamMemberPermissionChecks.get_all_available_team_member_permissions(),
)
@ -1993,27 +2025,53 @@ async def update_team_member_permissions(
"""
Update the team member permissions for a team
"""
from litellm.proxy.proxy_server import prisma_client
from litellm.proxy.proxy_server import (
prisma_client,
proxy_logging_obj,
user_api_key_cache,
)
if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"})
team_row = await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": data.team_id}
## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
existing_team_row = await get_team_object(
team_id=data.team_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=None,
proxy_logging_obj=proxy_logging_obj,
check_cache_only=False,
check_db_only=True,
)
if team_row is None:
if existing_team_row is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={data.team_id}"},
detail={"error": f"Team not found for team_id={data.team_id}"},
)
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
if (
hasattr(user_api_key_dict, "user_role")
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
and not _is_user_team_admin(
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
)
and not _is_available_team(
team_id=complete_team_data.team_id,
user_api_key_dict=user_api_key_dict,
)
):
raise HTTPException(
status_code=403,
detail={"error": "Only proxy admin can update team member permissions"},
detail={
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
"/team/member_add",
complete_team_data.team_id,
)
},
)
# Update the team member permissions
updated_team = await prisma_client.db.litellm_teamtable.update(
where={"team_id": data.team_id},

View file

@ -59,40 +59,47 @@ async def test_get_team_permissions_list_success(mock_db_client, mock_admin_auth
Test successful retrieval of team member permissions.
"""
test_team_id = "test-team-123"
permissions = ["/key/generate", "/key/update"]
mock_team_data = {
"team_id": test_team_id,
"team_alias": "Test Team",
"team_member_permissions": ["/key/generate", "/key/update"],
"team_member_permissions": permissions,
"spend": 0.0,
}
mock_team_row = MagicMock()
mock_team_row.model_dump.return_value = mock_team_data
mock_db_client.db.litellm_teamtable.find_unique = AsyncMock(
return_value=mock_team_row
)
# Override the dependency for this test
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
# Set attributes directly on the mock object
mock_team_row.team_id = test_team_id
mock_team_row.team_alias = "Test Team"
mock_team_row.team_member_permissions = permissions
mock_team_row.spend = 0.0
response = client.get(f"/team/permissions_list?team_id={test_team_id}")
# Mock the get_team_object function used in the endpoint
with patch(
"litellm.proxy.management_endpoints.team_endpoints.get_team_object",
new_callable=AsyncMock,
return_value=mock_team_row,
):
# Override the dependency for this test
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
assert response.status_code == 200
response_data = response.json()
assert response_data["team_id"] == test_team_id
assert (
response_data["team_member_permissions"]
== mock_team_data["team_member_permissions"]
)
assert (
response_data["all_available_permissions"]
== TeamMemberPermissionChecks.get_all_available_team_member_permissions()
)
mock_db_client.db.litellm_teamtable.find_unique.assert_awaited_once_with(
where={"team_id": test_team_id}
)
response = client.get(f"/team/permissions_list?team_id={test_team_id}")
# Clean up dependency override
app.dependency_overrides = {}
assert response.status_code == 200
response_data = response.json()
assert response_data["team_id"] == test_team_id
assert (
response_data["team_member_permissions"]
== mock_team_data["team_member_permissions"]
)
assert (
response_data["all_available_permissions"]
== TeamMemberPermissionChecks.get_all_available_team_member_permissions()
)
# Clean up dependency override
app.dependency_overrides = {}
# Test for /team/permissions_update endpoint (POST)
@ -102,15 +109,17 @@ async def test_update_team_permissions_success(mock_db_client, mock_admin_auth):
Test successful update of team member permissions by an admin.
"""
test_team_id = "test-team-456"
update_permissions = ["/key/generate", "/key/update"]
update_payload = {
"team_id": test_team_id,
"team_member_permissions": ["/key/generate", "/key/update"],
"team_member_permissions": update_permissions,
}
existing_permissions = ["/key/list"]
mock_existing_team_data = {
"team_id": test_team_id,
"team_alias": "Existing Team",
"team_member_permissions": ["/key/list"],
"team_member_permissions": existing_permissions,
"spend": 0.0,
"models": [],
}
@ -121,41 +130,50 @@ async def test_update_team_permissions_success(mock_db_client, mock_admin_auth):
mock_existing_team_row = MagicMock(spec=LiteLLM_TeamTable)
mock_existing_team_row.model_dump.return_value = mock_existing_team_data
# Set attributes directly if model_dump isn't enough for LiteLLM_TeamTable usage
for key, value in mock_existing_team_data.items():
setattr(mock_existing_team_row, key, value)
# Set attributes directly on the existing team mock
mock_existing_team_row.team_id = test_team_id
mock_existing_team_row.team_alias = "Existing Team"
mock_existing_team_row.team_member_permissions = existing_permissions
mock_existing_team_row.spend = 0.0
mock_existing_team_row.models = []
mock_updated_team_row = MagicMock(spec=LiteLLM_TeamTable)
mock_updated_team_row.model_dump.return_value = mock_updated_team_data
# Set attributes directly if model_dump isn't enough for LiteLLM_TeamTable usage
for key, value in mock_updated_team_data.items():
setattr(mock_updated_team_row, key, value)
mock_db_client.db.litellm_teamtable.find_unique = AsyncMock(
return_value=mock_existing_team_row
)
mock_db_client.db.litellm_teamtable.update = AsyncMock(
return_value=mock_updated_team_row
)
# Set attributes directly on the updated team mock
mock_updated_team_row.team_id = test_team_id
mock_updated_team_row.team_alias = "Existing Team"
mock_updated_team_row.team_member_permissions = update_permissions
mock_updated_team_row.spend = 0.0
mock_updated_team_row.models = []
# Override the dependency for this test
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
# Mock the get_team_object function used in the endpoint
with patch(
"litellm.proxy.management_endpoints.team_endpoints.get_team_object",
new_callable=AsyncMock,
return_value=mock_existing_team_row,
):
# Mock the database update function
mock_db_client.db.litellm_teamtable.update = AsyncMock(
return_value=mock_updated_team_row
)
response = client.post("/team/permissions_update", json=update_payload)
# Override the dependency for this test
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
assert response.status_code == 200
response_data = response.json()
response = client.post("/team/permissions_update", json=update_payload)
# Use model_dump for comparison if the endpoint returns the Prisma model directly
assert response_data == mock_updated_team_row.model_dump()
assert response.status_code == 200
response_data = response.json()
mock_db_client.db.litellm_teamtable.find_unique.assert_awaited_once_with(
where={"team_id": test_team_id}
)
mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
where={"team_id": test_team_id},
data={"team_member_permissions": update_payload["team_member_permissions"]},
)
# Use model_dump for comparison if the endpoint returns the Prisma model directly
assert response_data == mock_updated_team_row.model_dump()
# Clean up dependency override
app.dependency_overrides = {}
mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
where={"team_id": test_team_id},
data={"team_member_permissions": update_payload["team_member_permissions"]},
)
# Clean up dependency override
app.dependency_overrides = {}

View file

@ -18,6 +18,7 @@ import {
Input,
Select as Select2,
message,
InputNumber,
} from "antd";
import NumericalInput from "./shared/numerical_input";
@ -113,7 +114,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
tooltip="(float) - Spend of all LLM calls completed by this user"
help="Across all keys (including keys with team_id)."
>
<NumericalInput min={0} step={1} />
<InputNumber min={0} step={0.01} />
</Form.Item>
<Form.Item
@ -122,7 +123,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
tooltip="(float) - Maximum budget of this user"
help="Maximum budget of this user."
>
<NumericalInput min={0} step={1} />
<NumericalInput min={0} step={0.01} />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>