Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3140,7 +3140,7 @@ async def _register_pass_through_endpoint(
forward_headers: Final = endpoint_data.get("forward_headers")
merge_query_params: Final = endpoint_data.get("merge_query_params")
default_query_params: Final = endpoint_data.get("default_query_params")
auth: Final[bool | str | None] = endpoint_data.get("auth")
auth: Final[bool | str | None] = endpoint_data.get("auth", True)
dependencies = None
auth_enforced: Final = auth is not None and str(auth).lower() == "true"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@
``auth`` key still requires authentication.
"""

from itertools import chain
from typing import Final
from unittest.mock import AsyncMock, MagicMock

import pytest
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.routing import APIRoute


from litellm.proxy._types import PassThroughGenericEndpoint
from litellm.proxy._types import LiteLLMRoutes, PassThroughGenericEndpoint
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
user_api_key_auth,
)
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
InitPassThroughEndpointHelpers,
_register_pass_through_endpoint,
)
from litellm.proxy.utils import ProxyLogging


def test_passthrough_auth_defaults_to_true():
Expand Down Expand Up @@ -131,3 +138,133 @@ async def test_runtime_check_explicit_auth_false_still_skips_validation():
)

assert isinstance(result, UserAPIKeyAuth)


def _route_level_dependencies(app: FastAPI, path: str) -> list[object]:
matching: Final = [route for route in app.routes if isinstance(route, APIRoute) and route.path == path]
return [dep.dependency for dep in chain.from_iterable(route.dependencies for route in matching)]


def _is_proxy_only_error(proxy_log: ProxyLogging, original_exception: Exception, route: str) -> bool:
is_proxy_only: Final = (
proxy_log._is_proxy_only_llm_api_error # pyright: ignore[reportPrivateUsage] # only observable as a DB row
)
return is_proxy_only(original_exception=original_exception, route=route)


_CONFIG_OMITTED: Final = {
"id": "cfg-omitted",
"path": "/cfg-omitted-pt",
"target": "http://upstream.invalid",
"include_subpath": True,
}
_CONFIG_AUTH_FALSE: Final = {
"id": "cfg-authfalse",
"path": "/cfg-authfalse-pt",
"target": "http://upstream.invalid",
"include_subpath": True,
"auth": False,
}
_DB_DEFAULT: Final = PassThroughGenericEndpoint(
id="db-default",
path="/db-default-pt",
target="http://upstream.invalid",
include_subpath=True,
)
_DB_AUTH_FALSE: Final = PassThroughGenericEndpoint(
id="db-authfalse",
path="/db-authfalse-pt",
target="http://upstream.invalid",
include_subpath=True,
auth=False,
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
("endpoint", "endpoint_id", "path", "enforced", "route"),
[
pytest.param(
_CONFIG_OMITTED,
"cfg-omitted",
"/cfg-omitted-pt",
True,
"/cfg-omitted-pt",
id="config-omitted-exact",
),
pytest.param(
_CONFIG_OMITTED,
"cfg-omitted",
"/cfg-omitted-pt",
True,
"/cfg-omitted-pt/sub",
id="config-omitted-sub",
),
pytest.param(
_CONFIG_AUTH_FALSE,
"cfg-authfalse",
"/cfg-authfalse-pt",
False,
"/cfg-authfalse-pt",
id="config-authfalse-exact",
),
pytest.param(
_CONFIG_AUTH_FALSE,
"cfg-authfalse",
"/cfg-authfalse-pt",
False,
"/cfg-authfalse-pt/sub",
id="config-authfalse-sub",
),
pytest.param(_DB_DEFAULT, "db-default", "/db-default-pt", True, "/db-default-pt", id="db-default-exact"),
pytest.param(_DB_DEFAULT, "db-default", "/db-default-pt", True, "/db-default-pt/sub", id="db-default-sub"),
pytest.param(
_DB_AUTH_FALSE,
"db-authfalse",
"/db-authfalse-pt",
False,
"/db-authfalse-pt",
id="db-authfalse-exact",
),
pytest.param(
_DB_AUTH_FALSE,
"db-authfalse",
"/db-authfalse-pt",
False,
"/db-authfalse-pt/sub",
id="db-authfalse-sub",
),
],
)
async def test_pass_through_registration_auth_enforcement(
endpoint: dict[str, object] | PassThroughGenericEndpoint,
endpoint_id: str,
path: str,
enforced: bool,
route: str,
):
app: Final = FastAPI()
try:
await _register_pass_through_endpoint(
endpoint=endpoint,
app=app,
premium_user=False,
visited_endpoints=set(),
)
upstream_error: Final = HTTPException(
status_code=403,
detail="Upstream passthrough request failed with status 403",
)
proxy_logging: Final = ProxyLogging(user_api_key_cache=UserApiKeyCache())
exact_dependencies: Final = _route_level_dependencies(app, path)
subpath_dependencies: Final = _route_level_dependencies(app, f"{path}/{{subpath:path}}")

assert (path in LiteLLMRoutes.openai_routes.value) == enforced
assert (f"{path}/*" in LiteLLMRoutes.openai_routes.value) == enforced
assert RouteChecks.is_llm_api_route(route=route) == enforced
assert RouteChecks.is_auth_enforced_pass_through_route(route=route, method="POST") == enforced
assert _is_proxy_only_error(proxy_logging, upstream_error, route) == enforced
assert (user_api_key_auth in exact_dependencies) == enforced
assert (user_api_key_auth in subpath_dependencies) == enforced
finally:
InitPassThroughEndpointHelpers.remove_endpoint_routes(endpoint_id)
Loading