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

View file

@ -1952,30 +1952,62 @@ async def team_member_permissions(
""" """
Get the team member permissions for a team 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: if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"}) raise HTTPException(status_code=500, detail={"error": "No db connected"})
team_row = await prisma_client.db.litellm_teamtable.find_unique( ## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
where={"team_id": team_id} 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 existing_team_row is None:
if team_row is None:
raise HTTPException( raise HTTPException(
status_code=404, 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()) complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
if team_obj.team_member_permissions is None:
team_obj.team_member_permissions = ( 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() TeamMemberPermissionChecks.default_team_member_permissions()
) )
return GetTeamMemberPermissionsResponse( return GetTeamMemberPermissionsResponse(
team_id=team_id, 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(), 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 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: if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"}) raise HTTPException(status_code=500, detail={"error": "No db connected"})
team_row = await prisma_client.db.litellm_teamtable.find_unique( ## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
where={"team_id": data.team_id} 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 existing_team_row is None:
if team_row is None:
raise HTTPException( raise HTTPException(
status_code=404, 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( raise HTTPException(
status_code=403, 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 # Update the team member permissions
updated_team = await prisma_client.db.litellm_teamtable.update( updated_team = await prisma_client.db.litellm_teamtable.update(
where={"team_id": data.team_id}, 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 successful retrieval of team member permissions.
""" """
test_team_id = "test-team-123" test_team_id = "test-team-123"
permissions = ["/key/generate", "/key/update"]
mock_team_data = { mock_team_data = {
"team_id": test_team_id, "team_id": test_team_id,
"team_alias": "Test Team", "team_alias": "Test Team",
"team_member_permissions": ["/key/generate", "/key/update"], "team_member_permissions": permissions,
"spend": 0.0, "spend": 0.0,
} }
mock_team_row = MagicMock() mock_team_row = MagicMock()
mock_team_row.model_dump.return_value = mock_team_data 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 # Set attributes directly on the mock object
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth 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 = client.get(f"/team/permissions_list?team_id={test_team_id}")
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}
)
# Clean up dependency override assert response.status_code == 200
app.dependency_overrides = {} 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) # 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 successful update of team member permissions by an admin.
""" """
test_team_id = "test-team-456" test_team_id = "test-team-456"
update_permissions = ["/key/generate", "/key/update"]
update_payload = { update_payload = {
"team_id": test_team_id, "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 = { mock_existing_team_data = {
"team_id": test_team_id, "team_id": test_team_id,
"team_alias": "Existing Team", "team_alias": "Existing Team",
"team_member_permissions": ["/key/list"], "team_member_permissions": existing_permissions,
"spend": 0.0, "spend": 0.0,
"models": [], "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 = MagicMock(spec=LiteLLM_TeamTable)
mock_existing_team_row.model_dump.return_value = mock_existing_team_data 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(): # Set attributes directly on the existing team mock
setattr(mock_existing_team_row, key, value) 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 = MagicMock(spec=LiteLLM_TeamTable)
mock_updated_team_row.model_dump.return_value = mock_updated_team_data 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( # Set attributes directly on the updated team mock
return_value=mock_existing_team_row mock_updated_team_row.team_id = test_team_id
) mock_updated_team_row.team_alias = "Existing Team"
mock_db_client.db.litellm_teamtable.update = AsyncMock( mock_updated_team_row.team_member_permissions = update_permissions
return_value=mock_updated_team_row mock_updated_team_row.spend = 0.0
) mock_updated_team_row.models = []
# Override the dependency for this test # Mock the get_team_object function used in the endpoint
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth 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 = client.post("/team/permissions_update", json=update_payload)
response_data = response.json()
# Use model_dump for comparison if the endpoint returns the Prisma model directly assert response.status_code == 200
assert response_data == mock_updated_team_row.model_dump() response_data = response.json()
mock_db_client.db.litellm_teamtable.find_unique.assert_awaited_once_with( # Use model_dump for comparison if the endpoint returns the Prisma model directly
where={"team_id": test_team_id} assert response_data == mock_updated_team_row.model_dump()
)
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 mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
app.dependency_overrides = {} 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, Input,
Select as Select2, Select as Select2,
message, message,
InputNumber,
} from "antd"; } from "antd";
import NumericalInput from "./shared/numerical_input"; 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" tooltip="(float) - Spend of all LLM calls completed by this user"
help="Across all keys (including keys with team_id)." help="Across all keys (including keys with team_id)."
> >
<NumericalInput min={0} step={1} /> <InputNumber min={0} step={0.01} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -122,7 +123,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
tooltip="(float) - Maximum budget of this user" tooltip="(float) - Maximum budget of this user"
help="Maximum budget of this user." help="Maximum budget of this user."
> >
<NumericalInput min={0} step={1} /> <NumericalInput min={0} step={0.01} />
</Form.Item> </Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}> <div style={{ textAlign: "right", marginTop: "10px" }}>