mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 03:04:13 +00:00
[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:
parent
2ed63da5f8
commit
89dfb42697
4 changed files with 155 additions and 74 deletions
|
@ -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",
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -59,18 +59,28 @@ 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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
# Override the dependency for this test
|
||||||
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
|
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
|
||||||
|
|
||||||
|
@ -87,9 +97,6 @@ async def test_get_team_permissions_list_success(mock_db_client, mock_admin_auth
|
||||||
response_data["all_available_permissions"]
|
response_data["all_available_permissions"]
|
||||||
== TeamMemberPermissionChecks.get_all_available_team_member_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
|
# Clean up dependency override
|
||||||
app.dependency_overrides = {}
|
app.dependency_overrides = {}
|
||||||
|
@ -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,19 +130,31 @@ 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_updated_team_row.team_member_permissions = update_permissions
|
||||||
|
mock_updated_team_row.spend = 0.0
|
||||||
|
mock_updated_team_row.models = []
|
||||||
|
|
||||||
|
# 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(
|
mock_db_client.db.litellm_teamtable.update = AsyncMock(
|
||||||
return_value=mock_updated_team_row
|
return_value=mock_updated_team_row
|
||||||
)
|
)
|
||||||
|
@ -149,9 +170,6 @@ async def test_update_team_permissions_success(mock_db_client, mock_admin_auth):
|
||||||
# Use model_dump for comparison if the endpoint returns the Prisma model directly
|
# Use model_dump for comparison if the endpoint returns the Prisma model directly
|
||||||
assert response_data == mock_updated_team_row.model_dump()
|
assert response_data == mock_updated_team_row.model_dump()
|
||||||
|
|
||||||
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(
|
mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
|
||||||
where={"team_id": test_team_id},
|
where={"team_id": test_team_id},
|
||||||
data={"team_member_permissions": update_payload["team_member_permissions"]},
|
data={"team_member_permissions": update_payload["team_member_permissions"]},
|
||||||
|
|
|
@ -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" }}>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue