mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge cb322f8b42 into 98acc01e0d
This commit is contained in:
commit
e0a422260b
3 changed files with 89 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue