This commit is contained in:
Ben Younes 2026-11-07 01:03:09 +00:00 • committed by GitHub
commit e0a422260b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 13 deletions

View file

@ -1581,15 +1581,9 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints(
api_key = request.headers.get("litellm_user_api_key") or ""
if pass_through_endpoints is not None:
for endpoint in pass_through_endpoints:
if _request_matches_unauthenticated_pass_through(endpoint, route):
return UserAPIKeyAuth()
if isinstance(endpoint, dict) and endpoint.get("path", "") == route:
## IF AUTH DISABLED
# Default to True: a config dict with no ``auth`` key
# otherwise produced an unauthenticated forwarder. The
# Pydantic ``PassThroughGenericEndpoint.auth`` default
# is also True, but raw config dicts skip that path —
# so this runtime check has to default to True too.
if endpoint.get("auth", True) is not True:
return UserAPIKeyAuth()
## IF AUTH ENABLED
### IF CUSTOM PARSER REQUIRED
if endpoint.get("custom_auth_parser") is not None and endpoint.get("custom_auth_parser") == "langfuse":
@ -3610,6 +3604,18 @@ async def _load_team_member_default_model_budget(
)
def _request_matches_unauthenticated_pass_through(endpoint: object, route: str) -> bool:
if not isinstance(endpoint, dict):
return False
endpoint_data: Final = cast("dict[str, object]", endpoint)
if str(endpoint_data.get("auth", True)).lower() == "true":
return False
path: Final = endpoint_data.get("path")
if not isinstance(path, str) or not path:
return False
return route == path or (endpoint_data.get("include_subpath") is True and route.startswith(path.rstrip("/") + "/"))
@tracer.wrap()
async def run_centralized_common_checks(
user_api_key_auth_obj: UserAPIKeyAuth,
@ -3672,7 +3678,7 @@ async def run_centralized_common_checks(
pass_through_endpoints: Final = general_settings.get("pass_through_endpoints", None)
if not force_virtual_key_checks and pass_through_endpoints is not None:
for endpoint in pass_through_endpoints:
if isinstance(endpoint, dict) and endpoint.get("path", "") == route and endpoint.get("auth") is not True:
if _request_matches_unauthenticated_pass_through(endpoint, route):
return
# No-auth dev mode: master_key unset AND no JWT/OAuth2 auth

View file

@ -26,7 +26,7 @@ from litellm.redis.stores.connection import connect
from litellm.redis.stores.spend_counters import SpendCounterStore
from tests.unit.redis.stores.spend_counters_double import FakeSpendCounters
from fastapi import HTTPException, Request, status
from starlette.datastructures import UploadFile
from starlette.datastructures import URL, UploadFile
import litellm
import litellm.proxy.proxy_server
@ -4846,6 +4846,40 @@ def _proxy_attrs_for_centralized_checks(user_custom_auth=None, flag=False, maste
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"endpoint, route, expect_common_checks",
[
({"path": "/orx", "include_subpath": True, "auth": False}, "/orx/rerank", False),
({"path": "/orx", "include_subpath": True, "auth": False}, "/orx", False),
({"path": "/orx", "auth": False}, "/orx/rerank", True),
({"path": "/orx", "include_subpath": True, "auth": False}, "/orxfoo", True),
({"path": "/orx", "include_subpath": True}, "/orx/rerank", True),
({"path": "/orx", "include_subpath": True, "auth": True}, "/orx/rerank", True),
({"path": "/orx", "include_subpath": True, "auth": "true"}, "/orx/rerank", True),
],
)
async def test_centralized_common_checks_include_subpath_pass_through_authz(
endpoint: dict[str, object], route: str, expect_common_checks: bool
) -> None:
token = UserAPIKeyAuth(api_key="sk-test", user_id="u1")
request = Request(scope={"type": "http", "method": "POST", "headers": [], "query_string": b""})
request._url = URL(url=route)
attrs = _proxy_attrs_for_centralized_checks()
attrs["general_settings"] = {"pass_through_endpoints": [endpoint]}
with (
patch.multiple(litellm.proxy.proxy_server, **attrs),
patch("litellm.proxy.auth.user_api_key_auth.common_checks", new_callable=AsyncMock) as mock_checks,
):
await run_centralized_common_checks(
user_api_key_auth_obj=token,
request=request,
request_data={},
route=route,
)
assert bool(mock_checks.await_count) is expect_common_checks
@pytest.mark.asyncio
async def test_centralized_common_checks_runs_for_standard_auth():
"""Regardless of which _user_api_key_auth_builder path returned, the

View file

@ -29,7 +29,7 @@ from fastapi.testclient import TestClient
from starlette.requests import Request
from litellm.proxy._types import PassThroughGenericEndpoint, ProxyException
from litellm.proxy._types import PassThroughGenericEndpoint, ProxyException, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
@ -116,12 +116,48 @@ async def test_runtime_check_treats_missing_auth_key_as_authenticated():
assert result == "sk-9876"
@pytest.mark.asyncio
async def test_runtime_check_unauthenticated_subpath_skips_validation() -> None:
request: Final = MagicMock()
request.headers = {}
endpoint: Final = {
"path": "/public-webhook",
"target": "https://example.com",
"include_subpath": True,
"auth": False,
}
result: Final = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/public-webhook/event",
pass_through_endpoints=[endpoint],
api_key="",
)
assert isinstance(result, UserAPIKeyAuth)
@pytest.mark.asyncio
@pytest.mark.parametrize("auth", ["true", "TRUE"])
async def test_runtime_check_string_auth_true_requires_validation(auth: str) -> None:
request: Final = MagicMock()
request.headers = {}
endpoint: Final = {"path": "/forwarder", "target": "https://example.com", "auth": auth}
result: Final = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/forwarder",
pass_through_endpoints=[endpoint],
api_key="sk-9876",
)
assert result == "sk-9876"
@pytest.mark.asyncio
async def test_runtime_check_explicit_auth_false_still_skips_validation():
# Operators who explicitly set ``auth: False`` get the legacy
# behaviour — an empty UserAPIKeyAuth, no key required.
from litellm.proxy._types import UserAPIKeyAuth
request = MagicMock()
request.headers = {}
raw_endpoint_auth_false = {