mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(proxy): require proxy admin role on GET /customer/info
The handler looked up any end_user_id in the global end-user table without checking the caller's role, so an org admin whose route gate passed via org_admin_allowed_routes could read every customer record in the deployment. Gate it on PROXY_ADMIN / PROXY_ADMIN_VIEW_ONLY, matching /customer/list Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
658f50663d
commit
b2eef615de
2 changed files with 58 additions and 4 deletions
|
|
@ -503,9 +503,10 @@ async def new_end_user(
|
|||
)
|
||||
async def end_user_info(
|
||||
end_user_id: str = fastapi.Query(description="End User ID in the request parameters"),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> CustomerResponse:
|
||||
"""
|
||||
Get information about an end-user. An `end_user` is a customer (external user) of the proxy.
|
||||
[Admin-only] Get information about an end-user. An `end_user` is a customer (external user) of the proxy.
|
||||
|
||||
Parameters:
|
||||
- end_user_id (str, required): The unique identifier for the end-user
|
||||
|
|
@ -519,6 +520,15 @@ async def end_user_info(
|
|||
try:
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN
|
||||
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail={"error": f"Admin-only endpoint. Your user role={user_api_key_dict.user_role}"},
|
||||
)
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
|
|
|
|||
|
|
@ -177,6 +177,52 @@ def test_info_customer_not_found(mock_prisma_client, mock_user_api_key_auth):
|
|||
assert response_json["error"]["code"] == "404"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_role",
|
||||
[LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, LitellmUserRoles.TEAM],
|
||||
)
|
||||
def test_info_customer_non_admin_is_rejected(mock_prisma_client, user_role):
|
||||
"""
|
||||
Security regression: end users live in one global table with no tenant
|
||||
scoping, so a non-admin caller (for example an org admin whose route gate
|
||||
passes via `org_admin_allowed_routes`) must not be able to read another
|
||||
tenant's customer record through /customer/info.
|
||||
"""
|
||||
mock_prisma_client.db.litellm_endusertable.find_first = AsyncMock(
|
||||
return_value=LiteLLM_EndUserTable(user_id="victim-customer", alias="Victim", blocked=False)
|
||||
)
|
||||
original_overrides = app.dependency_overrides.copy()
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_id="org-admin-user", user_role=user_role)
|
||||
try:
|
||||
response = client.get(
|
||||
"/customer/info?end_user_id=victim-customer",
|
||||
headers={"Authorization": "Bearer org-admin-key"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides = original_overrides
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Admin-only endpoint" in response.json()["error"]["message"]
|
||||
mock_prisma_client.db.litellm_endusertable.find_first.assert_not_called()
|
||||
|
||||
|
||||
def test_info_customer_admin_viewer_allowed(mock_prisma_client):
|
||||
mock_prisma_client.db.litellm_endusertable.find_first = AsyncMock(
|
||||
return_value=LiteLLM_EndUserTable(user_id="c1", alias="Customer One", blocked=False)
|
||||
)
|
||||
original_overrides = app.dependency_overrides.copy()
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_id="viewer", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
|
||||
)
|
||||
try:
|
||||
response = client.get("/customer/info?end_user_id=c1", headers={"Authorization": "Bearer viewer-key"})
|
||||
finally:
|
||||
app.dependency_overrides = original_overrides
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["user_id"] == "c1"
|
||||
|
||||
|
||||
def test_delete_customer_not_found(mock_prisma_client, mock_user_api_key_auth):
|
||||
"""
|
||||
Test that delete_end_user raises a 404 ProxyException when user_ids do not exist.
|
||||
|
|
@ -795,9 +841,7 @@ def test_char_new_body(mock_prisma_client, mock_user_api_key_auth):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bad_duration", ["0s", "-5m"])
|
||||
def test_customer_new_rejects_a_duration_that_never_advances(
|
||||
mock_prisma_client, mock_user_api_key_auth, bad_duration
|
||||
):
|
||||
def test_customer_new_rejects_a_duration_that_never_advances(mock_prisma_client, mock_user_api_key_auth, bad_duration):
|
||||
"""A zero-length window resets to "now", leaving the customer's budget row
|
||||
permanently due for the reset job to re-read every tick."""
|
||||
mock_prisma_client.db.litellm_endusertable.create = AsyncMock(return_value=_row(_FULL_DB_ROW))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue