This commit is contained in:
Joris Stander 2026-10-20 01:50:12 +00:00 • committed by GitHub
commit d65dff2633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 80 additions and 7 deletions

View file

@ -184,7 +184,7 @@ from litellm.router_utils.fallback_event_handlers import (
fallbacks_disabled_for_request,
get_fallback_model_group_for_lookup_groups,
)
from litellm.secret_managers.main import get_secret_bool
from litellm.secret_managers.main import get_secret_bool, get_secret_str, normalize_nonempty_secret_str
from litellm.types.services import ServiceTypes
try:
@ -2737,6 +2737,18 @@ async def user_api_key_auth_builder(
if (
prisma_client is None
): # if both master key + user key submitted, and user key != master key, and no db connected, raise an error
if normalize_nonempty_secret_str(get_secret_str("DATABASE_URL")) is None:
# No DB configured: an auth error, so allow_requests_on_db_unavailable can't fail open
raise ProxyException(
message=(
"Authentication Error, Invalid proxy server token passed. Token does not match "
"the configured master key, and no database is configured to look up virtual keys. "
"Set DATABASE_URL to enable virtual keys."
),
type=ProxyErrorTypes.auth_error,
code=status.HTTP_401_UNAUTHORIZED,
param=None,
)
raise ProxyException(
message="No connected db.",
type=ProxyErrorTypes.no_db_connection,

View file

@ -12073,7 +12073,7 @@ class ProxyStartupEvent:
"""
connected_client: PrismaClient | None = None
try:
if database_url is None:
if database_url is None or not database_url.strip():
return None
prisma_client = PrismaClient(database_url=database_url, proxy_logging_obj=proxy_logging_obj)

View file

@ -476,9 +476,9 @@ def test_is_api_route_allowed(route, user_role, expected_result):
@pytest.mark.asyncio
async def test_auth_not_connected_to_db():
async def test_auth_not_connected_to_db(monkeypatch: pytest.MonkeyPatch):
"""
ensure requests don't fail when `prisma_client` = None
ensure requests don't fail when `prisma_client` = None but a database is configured
"""
from fastapi import Request
from starlette.datastructures import URL
@ -486,6 +486,7 @@ async def test_auth_not_connected_to_db():
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.proxy_server import hash_token, user_api_key_cache
monkeypatch.setenv("DATABASE_URL", "postgresql://unreachable.invalid:5432/litellm")
user_key = "sk-98765678"
setattr(litellm.proxy.proxy_server, "user_api_key_cache", user_api_key_cache)
@ -506,6 +507,46 @@ async def test_auth_not_connected_to_db():
assert valid_token.token == "failed-to-connect-to-db"
@pytest.mark.asyncio
@pytest.mark.parametrize("allow_requests_on_db_unavailable", [True, False])
@pytest.mark.parametrize("database_url", [None, "", " "])
async def test_auth_no_db_configured_rejects_non_master_key(
allow_requests_on_db_unavailable: bool, database_url: str | None, monkeypatch: pytest.MonkeyPatch
):
"""
With no database configured (DATABASE_URL unset or empty), a key that isn't the master key is an
auth error (401), and allow_requests_on_db_unavailable must not fail open: there is no outage to ride out.
"""
from fastapi import Request
from starlette.datastructures import URL
from litellm.proxy._types import ProxyErrorTypes, ProxyException
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.proxy_server import user_api_key_cache
if database_url is None:
monkeypatch.delenv("DATABASE_URL", raising=False)
else:
monkeypatch.setenv("DATABASE_URL", database_url)
setattr(litellm.proxy.proxy_server, "user_api_key_cache", user_api_key_cache)
setattr(litellm.proxy.proxy_server, "master_key", MASTER_KEY)
setattr(litellm.proxy.proxy_server, "prisma_client", None)
setattr(
litellm.proxy.proxy_server,
"general_settings",
{"allow_requests_on_db_unavailable": allow_requests_on_db_unavailable},
)
request = Request(scope={"type": "http", "method": "POST", "path": "/chat/completions", "headers": []})
request._url = URL(url="/chat/completions")
with pytest.raises(ProxyException) as exc_info:
await user_api_key_auth(request=request, api_key="Bearer sk-not-the-master-key")
assert exc_info.value.type == ProxyErrorTypes.auth_error
assert exc_info.value.code == "401"
def _assert_api_key_from_custom_header(headers, custom_header_name, expected_api_key):
verbose_proxy_logger.setLevel(logging.DEBUG)
request = MagicMock(spec=Request)

View file

@ -12502,10 +12502,11 @@ async def test_lens_oauth2_validates_identity_without_inference_user_field(
[("/lens", False, True), ("/lens/traces", True, False), ("/lens-other", True, True)],
)
async def test_lens_oauth2_dispatch_preserves_opt_in_premium_and_route_boundary(
route: str, enabled: bool, premium: bool
route: str, enabled: bool, premium: bool, monkeypatch: pytest.MonkeyPatch
) -> None:
from starlette.requests import Request
monkeypatch.delenv("DATABASE_URL", raising=False)
request: Final = Request({"type": "http", "method": "GET", "path": route, "headers": [], "query_string": b""})
with (
patch("litellm.proxy.proxy_server.general_settings", {"enable_oauth2_auth": enabled}),
@ -12522,8 +12523,8 @@ async def test_lens_oauth2_dispatch_preserves_opt_in_premium_and_route_boundary(
assert error.value.code == "403"
assert "premium" in error.value.message.lower()
else:
assert error.value.code == "400"
assert "No connected db" in error.value.message
assert error.value.code == "401"
assert "no database is configured" in error.value.message
@pytest.mark.asyncio

View file

@ -14105,6 +14105,25 @@ async def test_setup_prisma_client_retains_connected_client_when_startup_health_
assert result is mock_client
@pytest.mark.asyncio
@pytest.mark.parametrize("database_url", ["", " "])
async def test_setup_prisma_client_treats_an_empty_database_url_as_unset(database_url):
"""An empty DATABASE_URL (e.g. from ``${DATABASE_URL:-}``) can never connect, so no client is
created. Otherwise auth sees a configured-but-unreachable DB and allow_requests_on_db_unavailable
fails open for any key."""
from litellm.proxy.proxy_server import ProxyStartupEvent
mock_client = _mock_startup_prisma_client()
with patch.object(proxy_server_module, "PrismaClient", return_value=mock_client):
result = await ProxyStartupEvent._setup_prisma_client(
database_url=database_url,
proxy_logging_obj=MagicMock(),
user_api_key_cache=DualCache(),
)
assert result is None
@pytest.mark.asyncio
async def test_setup_prisma_client_arms_health_watchdog_before_startup_health_check(
monkeypatch,