Skip to content

Commit 89dfb42

Browse files
authored
[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
1 parent 2ed63da commit 89dfb42

File tree

4 files changed

+166
-85
lines changed

4 files changed

+166
-85
lines changed

litellm/proxy/_types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ class LiteLLMRoutes(enum.Enum):
371371
"/team/block",
372372
"/team/unblock",
373373
"/team/available",
374+
"/team/permissions_list",
375+
"/team/permissions_update",
374376
# model
375377
"/model/new",
376378
"/model/update",
@@ -456,6 +458,8 @@ class LiteLLMRoutes(enum.Enum):
456458
self_managed_routes = [
457459
"/team/member_add",
458460
"/team/member_delete",
461+
"/team/permissions_list",
462+
"/team/permissions_update",
459463
"/model/new",
460464
"/model/update",
461465
"/model/delete",

litellm/proxy/management_endpoints/team_endpoints.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,30 +1952,62 @@ async def team_member_permissions(
19521952
"""
19531953
Get the team member permissions for a team
19541954
"""
1955-
from litellm.proxy.proxy_server import prisma_client
1955+
from litellm.proxy.proxy_server import (
1956+
prisma_client,
1957+
proxy_logging_obj,
1958+
user_api_key_cache,
1959+
)
19561960

19571961
if prisma_client is None:
19581962
raise HTTPException(status_code=500, detail={"error": "No db connected"})
19591963

1960-
team_row = await prisma_client.db.litellm_teamtable.find_unique(
1961-
where={"team_id": team_id}
1964+
## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
1965+
existing_team_row = await get_team_object(
1966+
team_id=team_id,
1967+
prisma_client=prisma_client,
1968+
user_api_key_cache=user_api_key_cache,
1969+
parent_otel_span=None,
1970+
proxy_logging_obj=proxy_logging_obj,
1971+
check_cache_only=False,
1972+
check_db_only=True,
19621973
)
1963-
1964-
if team_row is None:
1974+
if existing_team_row is None:
19651975
raise HTTPException(
19661976
status_code=404,
1967-
detail={"error": f"Team not found, passed team_id={team_id}"},
1977+
detail={"error": f"Team not found for team_id={team_id}"},
19681978
)
19691979

1970-
team_obj = LiteLLM_TeamTable(**team_row.model_dump())
1971-
if team_obj.team_member_permissions is None:
1972-
team_obj.team_member_permissions = (
1980+
complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
1981+
1982+
if (
1983+
hasattr(user_api_key_dict, "user_role")
1984+
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
1985+
and not _is_user_team_admin(
1986+
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
1987+
)
1988+
and not _is_available_team(
1989+
team_id=complete_team_data.team_id,
1990+
user_api_key_dict=user_api_key_dict,
1991+
)
1992+
):
1993+
raise HTTPException(
1994+
status_code=403,
1995+
detail={
1996+
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
1997+
"/team/member_add",
1998+
complete_team_data.team_id,
1999+
)
2000+
},
2001+
)
2002+
2003+
if existing_team_row.team_member_permissions is None:
2004+
existing_team_row.team_member_permissions = (
19732005
TeamMemberPermissionChecks.default_team_member_permissions()
19742006
)
19752007

19762008
return GetTeamMemberPermissionsResponse(
19772009
team_id=team_id,
1978-
team_member_permissions=team_obj.team_member_permissions,
2010+
team_member_permissions=existing_team_row.team_member_permissions,
19792011
all_available_permissions=TeamMemberPermissionChecks.get_all_available_team_member_permissions(),
19802012
)
19812013

@@ -1993,27 +2025,53 @@ async def update_team_member_permissions(
19932025
"""
19942026
Update the team member permissions for a team
19952027
"""
1996-
from litellm.proxy.proxy_server import prisma_client
2028+
from litellm.proxy.proxy_server import (
2029+
prisma_client,
2030+
proxy_logging_obj,
2031+
user_api_key_cache,
2032+
)
19972033

19982034
if prisma_client is None:
19992035
raise HTTPException(status_code=500, detail={"error": "No db connected"})
20002036

2001-
team_row = await prisma_client.db.litellm_teamtable.find_unique(
2002-
where={"team_id": data.team_id}
2037+
## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN
2038+
existing_team_row = await get_team_object(
2039+
team_id=data.team_id,
2040+
prisma_client=prisma_client,
2041+
user_api_key_cache=user_api_key_cache,
2042+
parent_otel_span=None,
2043+
proxy_logging_obj=proxy_logging_obj,
2044+
check_cache_only=False,
2045+
check_db_only=True,
20032046
)
2004-
2005-
if team_row is None:
2047+
if existing_team_row is None:
20062048
raise HTTPException(
20072049
status_code=404,
2008-
detail={"error": f"Team not found, passed team_id={data.team_id}"},
2050+
detail={"error": f"Team not found for team_id={data.team_id}"},
20092051
)
20102052

2011-
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
2053+
complete_team_data = LiteLLM_TeamTable(**existing_team_row.model_dump())
2054+
2055+
if (
2056+
hasattr(user_api_key_dict, "user_role")
2057+
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
2058+
and not _is_user_team_admin(
2059+
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
2060+
)
2061+
and not _is_available_team(
2062+
team_id=complete_team_data.team_id,
2063+
user_api_key_dict=user_api_key_dict,
2064+
)
2065+
):
20122066
raise HTTPException(
20132067
status_code=403,
2014-
detail={"error": "Only proxy admin can update team member permissions"},
2068+
detail={
2069+
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
2070+
"/team/member_add",
2071+
complete_team_data.team_id,
2072+
)
2073+
},
20152074
)
2016-
20172075
# Update the team member permissions
20182076
updated_team = await prisma_client.db.litellm_teamtable.update(
20192077
where={"team_id": data.team_id},

tests/litellm/proxy/management_endpoints/test_team_endpoints.py

Lines changed: 82 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -59,40 +59,47 @@ async def test_get_team_permissions_list_success(mock_db_client, mock_admin_auth
5959
Test successful retrieval of team member permissions.
6060
"""
6161
test_team_id = "test-team-123"
62+
permissions = ["/key/generate", "/key/update"]
6263
mock_team_data = {
6364
"team_id": test_team_id,
6465
"team_alias": "Test Team",
65-
"team_member_permissions": ["/key/generate", "/key/update"],
66+
"team_member_permissions": permissions,
6667
"spend": 0.0,
6768
}
6869
mock_team_row = MagicMock()
6970
mock_team_row.model_dump.return_value = mock_team_data
70-
mock_db_client.db.litellm_teamtable.find_unique = AsyncMock(
71-
return_value=mock_team_row
72-
)
73-
74-
# Override the dependency for this test
75-
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
76-
77-
response = client.get(f"/team/permissions_list?team_id={test_team_id}")
78-
79-
assert response.status_code == 200
80-
response_data = response.json()
81-
assert response_data["team_id"] == test_team_id
82-
assert (
83-
response_data["team_member_permissions"]
84-
== mock_team_data["team_member_permissions"]
85-
)
86-
assert (
87-
response_data["all_available_permissions"]
88-
== TeamMemberPermissionChecks.get_all_available_team_member_permissions()
89-
)
90-
mock_db_client.db.litellm_teamtable.find_unique.assert_awaited_once_with(
91-
where={"team_id": test_team_id}
92-
)
93-
94-
# Clean up dependency override
95-
app.dependency_overrides = {}
71+
72+
# Set attributes directly on the mock object
73+
mock_team_row.team_id = test_team_id
74+
mock_team_row.team_alias = "Test Team"
75+
mock_team_row.team_member_permissions = permissions
76+
mock_team_row.spend = 0.0
77+
78+
# Mock the get_team_object function used in the endpoint
79+
with patch(
80+
"litellm.proxy.management_endpoints.team_endpoints.get_team_object",
81+
new_callable=AsyncMock,
82+
return_value=mock_team_row,
83+
):
84+
# Override the dependency for this test
85+
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
86+
87+
response = client.get(f"/team/permissions_list?team_id={test_team_id}")
88+
89+
assert response.status_code == 200
90+
response_data = response.json()
91+
assert response_data["team_id"] == test_team_id
92+
assert (
93+
response_data["team_member_permissions"]
94+
== mock_team_data["team_member_permissions"]
95+
)
96+
assert (
97+
response_data["all_available_permissions"]
98+
== TeamMemberPermissionChecks.get_all_available_team_member_permissions()
99+
)
100+
101+
# Clean up dependency override
102+
app.dependency_overrides = {}
96103

97104

98105
# Test for /team/permissions_update endpoint (POST)
@@ -102,15 +109,17 @@ async def test_update_team_permissions_success(mock_db_client, mock_admin_auth):
102109
Test successful update of team member permissions by an admin.
103110
"""
104111
test_team_id = "test-team-456"
112+
update_permissions = ["/key/generate", "/key/update"]
105113
update_payload = {
106114
"team_id": test_team_id,
107-
"team_member_permissions": ["/key/generate", "/key/update"],
115+
"team_member_permissions": update_permissions,
108116
}
109117

118+
existing_permissions = ["/key/list"]
110119
mock_existing_team_data = {
111120
"team_id": test_team_id,
112121
"team_alias": "Existing Team",
113-
"team_member_permissions": ["/key/list"],
122+
"team_member_permissions": existing_permissions,
114123
"spend": 0.0,
115124
"models": [],
116125
}
@@ -121,41 +130,50 @@ async def test_update_team_permissions_success(mock_db_client, mock_admin_auth):
121130

122131
mock_existing_team_row = MagicMock(spec=LiteLLM_TeamTable)
123132
mock_existing_team_row.model_dump.return_value = mock_existing_team_data
124-
# Set attributes directly if model_dump isn't enough for LiteLLM_TeamTable usage
125-
for key, value in mock_existing_team_data.items():
126-
setattr(mock_existing_team_row, key, value)
133+
134+
# Set attributes directly on the existing team mock
135+
mock_existing_team_row.team_id = test_team_id
136+
mock_existing_team_row.team_alias = "Existing Team"
137+
mock_existing_team_row.team_member_permissions = existing_permissions
138+
mock_existing_team_row.spend = 0.0
139+
mock_existing_team_row.models = []
127140

128141
mock_updated_team_row = MagicMock(spec=LiteLLM_TeamTable)
129142
mock_updated_team_row.model_dump.return_value = mock_updated_team_data
130-
# Set attributes directly if model_dump isn't enough for LiteLLM_TeamTable usage
131-
for key, value in mock_updated_team_data.items():
132-
setattr(mock_updated_team_row, key, value)
133-
134-
mock_db_client.db.litellm_teamtable.find_unique = AsyncMock(
135-
return_value=mock_existing_team_row
136-
)
137-
mock_db_client.db.litellm_teamtable.update = AsyncMock(
138-
return_value=mock_updated_team_row
139-
)
140-
141-
# Override the dependency for this test
142-
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
143-
144-
response = client.post("/team/permissions_update", json=update_payload)
145-
146-
assert response.status_code == 200
147-
response_data = response.json()
148-
149-
# Use model_dump for comparison if the endpoint returns the Prisma model directly
150-
assert response_data == mock_updated_team_row.model_dump()
151-
152-
mock_db_client.db.litellm_teamtable.find_unique.assert_awaited_once_with(
153-
where={"team_id": test_team_id}
154-
)
155-
mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
156-
where={"team_id": test_team_id},
157-
data={"team_member_permissions": update_payload["team_member_permissions"]},
158-
)
159-
160-
# Clean up dependency override
161-
app.dependency_overrides = {}
143+
144+
# Set attributes directly on the updated team mock
145+
mock_updated_team_row.team_id = test_team_id
146+
mock_updated_team_row.team_alias = "Existing Team"
147+
mock_updated_team_row.team_member_permissions = update_permissions
148+
mock_updated_team_row.spend = 0.0
149+
mock_updated_team_row.models = []
150+
151+
# Mock the get_team_object function used in the endpoint
152+
with patch(
153+
"litellm.proxy.management_endpoints.team_endpoints.get_team_object",
154+
new_callable=AsyncMock,
155+
return_value=mock_existing_team_row,
156+
):
157+
# Mock the database update function
158+
mock_db_client.db.litellm_teamtable.update = AsyncMock(
159+
return_value=mock_updated_team_row
160+
)
161+
162+
# Override the dependency for this test
163+
app.dependency_overrides[user_api_key_auth] = lambda: mock_admin_auth
164+
165+
response = client.post("/team/permissions_update", json=update_payload)
166+
167+
assert response.status_code == 200
168+
response_data = response.json()
169+
170+
# Use model_dump for comparison if the endpoint returns the Prisma model directly
171+
assert response_data == mock_updated_team_row.model_dump()
172+
173+
mock_db_client.db.litellm_teamtable.update.assert_awaited_once_with(
174+
where={"team_id": test_team_id},
175+
data={"team_member_permissions": update_payload["team_member_permissions"]},
176+
)
177+
178+
# Clean up dependency override
179+
app.dependency_overrides = {}

ui/litellm-dashboard/src/components/edit_user.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
Input,
1919
Select as Select2,
2020
message,
21+
InputNumber,
2122
} from "antd";
2223

2324
import NumericalInput from "./shared/numerical_input";
@@ -113,7 +114,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
113114
tooltip="(float) - Spend of all LLM calls completed by this user"
114115
help="Across all keys (including keys with team_id)."
115116
>
116-
<NumericalInput min={0} step={1} />
117+
<InputNumber min={0} step={0.01} />
117118
</Form.Item>
118119

119120
<Form.Item
@@ -122,7 +123,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
122123
tooltip="(float) - Maximum budget of this user"
123124
help="Maximum budget of this user."
124125
>
125-
<NumericalInput min={0} step={1} />
126+
<NumericalInput min={0} step={0.01} />
126127
</Form.Item>
127128

128129
<div style={{ textAlign: "right", marginTop: "10px" }}>

0 commit comments

Comments
 (0)