mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-25 05:54:53 +00:00
fix(proxy): avoid exception logs for expected auth denials
This commit is contained in:
parent
2655d1dd5e
commit
b8efc2d9a5
4 changed files with 75 additions and 19 deletions
|
|
@ -34,6 +34,14 @@ else:
|
|||
|
||||
|
||||
class UserAPIKeyAuthExceptionHandler:
|
||||
@staticmethod
|
||||
def _is_expected_authorization_denial(e: Exception) -> bool:
|
||||
if isinstance(e, HTTPException):
|
||||
return e.status_code == status.HTTP_403_FORBIDDEN
|
||||
if isinstance(e, ProxyException):
|
||||
return str(e.code) == str(status.HTTP_403_FORBIDDEN)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _handle_authentication_error(
|
||||
e: Exception,
|
||||
|
|
@ -94,13 +102,20 @@ class UserAPIKeyAuthExceptionHandler:
|
|||
request=request,
|
||||
use_x_forwarded_for=general_settings.get("use_x_forwarded_for", False),
|
||||
)
|
||||
verbose_proxy_logger.exception(
|
||||
"litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}".format(
|
||||
str(e),
|
||||
requester_ip,
|
||||
),
|
||||
extra={"requester_ip": requester_ip},
|
||||
log_message = "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}".format(
|
||||
str(e),
|
||||
requester_ip,
|
||||
)
|
||||
if UserAPIKeyAuthExceptionHandler._is_expected_authorization_denial(e):
|
||||
verbose_proxy_logger.warning(
|
||||
log_message,
|
||||
extra={"requester_ip": requester_ip},
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.exception(
|
||||
log_message,
|
||||
extra={"requester_ip": requester_ip},
|
||||
)
|
||||
|
||||
# Log this exception to OTEL, Datadog etc. Reuse the identity resolved
|
||||
# before the failure (team alias/id, metadata, user) so the failed span
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ class RouteChecks:
|
|||
route (str): The route being accessed
|
||||
|
||||
Raises:
|
||||
Exception: With user role and masked user_id information
|
||||
HTTPException: With user role and masked user_id information
|
||||
"""
|
||||
user_role = "unknown"
|
||||
user_id = "unknown"
|
||||
|
|
@ -238,8 +238,9 @@ class RouteChecks:
|
|||
user_id = user_obj.user_id or "unknown"
|
||||
|
||||
masked_user_id = RouteChecks._mask_user_id(user_id)
|
||||
raise Exception(
|
||||
f"Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route={route}. Your role={user_role}. Your user_id={masked_user_id}"
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route={route}. Your role={user_role}. Your user_id={masked_user_id}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, Request, status
|
||||
from prisma import errors as prisma_errors
|
||||
from fastapi import HTTPException, status
|
||||
from prisma.errors import (
|
||||
ClientNotConnectedError,
|
||||
DataError,
|
||||
|
|
@ -24,7 +22,6 @@ sys.path.insert(
|
|||
0, os.path.abspath("../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler
|
||||
|
||||
|
|
@ -272,6 +269,48 @@ async def test_handle_authentication_error_genuine_auth_failure_stays_401(auth_e
|
|||
assert int(exc_info.value.code) == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_authentication_error_expected_403_does_not_log_exception():
|
||||
handler = UserAPIKeyAuthExceptionHandler()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.auth.auth_exception_handler.verbose_proxy_logger.exception"
|
||||
) as mock_exception_log,
|
||||
patch(
|
||||
"litellm.proxy.auth.auth_exception_handler.verbose_proxy_logger.warning"
|
||||
) as mock_warning_log,
|
||||
patch(
|
||||
"litellm.proxy.auth.auth_exception_handler.seed_request_identity",
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj.post_call_failure_hook",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.proxy_server.general_settings",
|
||||
{"allow_requests_on_db_unavailable": False},
|
||||
),
|
||||
):
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await handler._handle_authentication_error(
|
||||
HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/config/list. Your role=internal_user.",
|
||||
),
|
||||
MagicMock(),
|
||||
{},
|
||||
"/config/list",
|
||||
None,
|
||||
"sk-non-admin",
|
||||
)
|
||||
|
||||
assert int(exc_info.value.code) == status.HTTP_403_FORBIDDEN
|
||||
mock_exception_log.assert_not_called()
|
||||
mock_warning_log.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_authentication_error_budget_exceeded():
|
||||
handler = UserAPIKeyAuthExceptionHandler()
|
||||
|
|
@ -336,7 +375,7 @@ async def test_route_passed_to_post_call_failure_hook():
|
|||
mock_span,
|
||||
mock_api_key,
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
asyncio.sleep(1)
|
||||
# Verify post_call_failure_hook was called with the correct route
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sys.path.insert(
|
|||
) # Adds the parent directory to the system path
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi import HTTPException, Request, status
|
||||
|
||||
from litellm.proxy._types import LiteLLM_UserTable, LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.route_checks import RouteChecks
|
||||
|
|
@ -34,7 +34,7 @@ def test_non_admin_config_update_route_rejected():
|
|||
request.query_params = {}
|
||||
|
||||
# Test that calling /config/update route raises HTTPException with 403 status
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
|
|
@ -44,13 +44,14 @@ def test_non_admin_config_update_route_rejected():
|
|||
request_data={},
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN
|
||||
# Verify the exception is raised with the correct message
|
||||
assert (
|
||||
"Only proxy admin can be used to generate, delete, update info for new keys/users/teams"
|
||||
in str(exc_info.value)
|
||||
in str(exc_info.value.detail)
|
||||
)
|
||||
assert "Route=/config/update" in str(exc_info.value)
|
||||
assert "Your role=internal_user" in str(exc_info.value)
|
||||
assert "Route=/config/update" in str(exc_info.value.detail)
|
||||
assert "Your role=internal_user" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue