This commit is contained in:
Frank Deng 2026-08-27 19:32:51 -05:00 committed by GitHub
commit 5c0ebcfafd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 112 additions and 1 deletions

View file

@ -690,6 +690,7 @@ class LiteLLMRoutes(enum.Enum):
"/spend/logs",
"/spend/logs/v2",
"/spend/logs/ui",
"/spend/logs/ui/{request_id}",
"/spend/logs/session/ui",
"/key/spend/report",
"/user/spend/report",

View file

@ -4288,7 +4288,14 @@ async def _assert_user_can_view_request_id(
"""
row: Final = await _find_spend_log_row(prisma_client, request_id)
if row is None:
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"error": "Not authorized to view spend log for request_id={}".format(
request_id
)
},
)
if row.user is not None and row.user == user_api_key_dict.user_id:
return

View file

@ -1977,6 +1977,38 @@ ADMIN_VIEWER_LOGS_PAGE_ROUTES = [
]
@pytest.mark.parametrize(
"user_role",
[
LitellmUserRoles.INTERNAL_USER,
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
],
)
def test_internal_user_can_access_spend_log_detail(
user_role: LitellmUserRoles,
) -> None:
user_obj = LiteLLM_UserTable(
user_id="internal_user",
user_email="user@example.com",
user_role=user_role.value,
)
valid_token = UserAPIKeyAuth(
user_id="internal_user",
user_role=user_role.value,
)
request = MagicMock(spec=Request)
request.query_params = {}
RouteChecks.non_proxy_admin_allowed_routes_check(
user_obj=user_obj,
_user_role=user_role.value,
route="/spend/logs/ui/request-id",
request=request,
valid_token=valid_token,
request_data={},
)
@pytest.mark.parametrize("route", ADMIN_VIEWER_LOGS_PAGE_ROUTES)
def test_proxy_admin_viewer_can_access_logs_page_endpoints(route):
"""

View file

@ -451,6 +451,77 @@ def test_ui_view_request_response_forbids_non_admin_without_db(client, monkeypat
app.dependency_overrides.pop(ps.user_api_key_auth, None)
def test_ui_view_request_response_forbids_missing_ownership_row(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
mock_prisma = MagicMock()
mock_prisma.db.litellm_spendlogs.find_unique = AsyncMock(return_value=None)
payload_logger = MagicMock()
payload_logger.get_request_response_payload = AsyncMock(
return_value={"response": "private response"}
)
get_custom_loggers = MagicMock(return_value=[payload_logger])
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
monkeypatch.setattr(
litellm.logging_callback_manager,
"get_active_additional_logging_utils_from_custom_logger",
get_custom_loggers,
)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="user_1",
)
try:
response = client.get(
"/spend/logs/ui/missing-request",
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 403
get_custom_loggers.assert_not_called()
payload_logger.get_request_response_payload.assert_not_awaited()
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
def test_ui_view_request_response_forbids_different_user(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
spend_log_row = MagicMock()
spend_log_row.user = "user_2"
spend_log_row.team_id = None
mock_prisma = MagicMock()
mock_prisma.db.litellm_spendlogs.find_unique = AsyncMock(return_value=spend_log_row)
payload_logger = MagicMock()
payload_logger.get_request_response_payload = AsyncMock(
return_value={"response": "private response"}
)
get_custom_loggers = MagicMock(return_value=[payload_logger])
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
monkeypatch.setattr(
litellm.logging_callback_manager,
"get_active_additional_logging_utils_from_custom_logger",
get_custom_loggers,
)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="user_1",
)
try:
response = client.get(
"/spend/logs/ui/other-user-request",
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 403
get_custom_loggers.assert_not_called()
payload_logger.get_request_response_payload.assert_not_awaited()
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
ignored_keys = [
"request_id",
"metadata.litellm_call_id",