Skip to content
Open
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
21 changes: 8 additions & 13 deletions litellm/proxy/auth/user_api_key_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@
from litellm.repositories.table_repositories import TeamMembershipRepository
from litellm.router_utils.common_utils import resolve_model_group_alias
from litellm.secret_managers.main import get_secret_bool
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
PassThroughAuthMode,
pass_through_auth_mode,
)
from litellm.types.services import ServiceTypes

try:
Expand Down Expand Up @@ -872,12 +876,7 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints(
for endpoint in pass_through_endpoints:
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:
if pass_through_auth_mode(endpoint.get("auth")) is PassThroughAuthMode.PUBLIC:
return UserAPIKeyAuth()
## IF AUTH ENABLED
### IF CUSTOM PARSER REQUIRED
Expand Down Expand Up @@ -2718,16 +2717,12 @@ async def _run_centralized_common_checks(
if route in LiteLLMRoutes.public_routes.value or route_in_additonal_public_routes(current_route=route):
return

# User-configured pass-through endpoints with ``auth: false`` are
# explicitly unauthenticated — the builder returns an empty
# UserAPIKeyAuth() and the request is forwarded as-is. Running
# common_checks on the empty token would reject the request as
# admin-only. The "auth" flag on the endpoint config is the
# contract; honor it.
pass_through_endpoints: Final = general_settings.get("pass_through_endpoints", None)
if 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 not isinstance(endpoint, dict) or endpoint.get("path", "") != route:
continue
if pass_through_auth_mode(endpoint.get("auth")) is PassThroughAuthMode.PUBLIC:
return

# No-auth dev mode: master_key unset AND no JWT/OAuth2 auth
Expand Down
42 changes: 42 additions & 0 deletions litellm/proxy/pass_through_endpoints/common_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
from collections.abc import Iterable, Mapping
from functools import lru_cache
from typing import Final

from fastapi import Request

from litellm._logging import verbose_proxy_logger
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
PassThroughAuthMode,
pass_through_auth_mode,
)

PASS_THROUGH_MISSING_AUTH_WARNING: Final = (
"pass_through_endpoints entry %r sets no `auth`: any valid LiteLLM key may call it. "
"Set `auth: true` to restrict it to keys granted `allowed_passthrough_routes`, "
"or `auth: false` to serve it without a key. "
"A future release will treat a missing `auth` as `auth: true`."
)

PASS_THROUGH_INVALID_AUTH_WARNING: Final = (
"pass_through_endpoints entry %r sets `auth: %s`, which is not a boolean, "
"so any valid LiteLLM key may call it. "
"Set `auth: true` to restrict it to keys granted `allowed_passthrough_routes`, "
"or `auth: false` to serve it without a key."
)


@lru_cache(maxsize=256)
def _warn_once_per_process(path: str, auth: str | None) -> None:
if auth is None:
verbose_proxy_logger.warning(PASS_THROUGH_MISSING_AUTH_WARNING, path)
return
verbose_proxy_logger.warning(PASS_THROUGH_INVALID_AUTH_WARNING, path, auth)


def _unenforced_auth(entry: Mapping[str, object]) -> tuple[str, str | None] | None:
auth: Final = entry.get("auth")
if pass_through_auth_mode(auth) is not PassThroughAuthMode.ANY_KEY:
return None
return (str(entry.get("path")), None if auth is None else repr(auth))


def warn_pass_through_entries_without_auth(entries: Iterable[Mapping[str, object]]) -> None:
for path, auth in filter(None, map(_unenforced_auth, entries)):
_warn_once_per_process(path, auth)


def get_litellm_virtual_key(request: Request) -> str:
"""
Expand Down
30 changes: 14 additions & 16 deletions litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@
LITELLM_PASS_THROUGH_ENDPOINT_MARKER,
LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY,
EndpointType,
PassThroughAuthMode,
PassthroughStandardLoggingPayload,
pass_through_auth_mode,
)
from litellm.types.utils import TRUSTED_CALLBACK_VARS_FIELD, Usage

Expand Down Expand Up @@ -3260,18 +3262,12 @@ 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")
dependencies = None
auth_enforced: Final = auth is not None and str(auth).lower() == "true"

if auth_enforced:
# Authentication on a pass-through endpoint used to be enterprise-only.
# That left OSS with no safe configuration: auth=True raised at startup
# unless the operator had a license. The safe option must always be free,
# and unauthenticated forwarding should require explicit opt-in.
dependencies = [Depends(user_api_key_auth)]
if path not in LiteLLMRoutes.openai_routes.value:
LiteLLMRoutes.openai_routes.value.append(path)
auth_mode: Final = pass_through_auth_mode(endpoint_data.get("auth"))
auth_enforced: Final = auth_mode is PassThroughAuthMode.GRANTED_KEYS
dependencies: Final = [Depends(user_api_key_auth)] if auth_enforced else None

if auth_mode is not PassThroughAuthMode.PUBLIC and path not in LiteLLMRoutes.openai_routes.value:
LiteLLMRoutes.openai_routes.value.append(path)

if target is None:
return
Expand Down Expand Up @@ -3305,10 +3301,9 @@ async def _register_pass_through_endpoint(
visited_endpoints.add(f"{endpoint_id}:exact:{path}:{methods_str}")

if endpoint_data.get("include_subpath", False) is True:
if auth is not None and str(auth).lower() == "true":
wildcard_path: Final = path.rstrip("/") + "/*"
if wildcard_path not in LiteLLMRoutes.openai_routes.value:
LiteLLMRoutes.openai_routes.value.append(wildcard_path)
wildcard_path: Final = path.rstrip("/") + "/*"
if auth_mode is not PassThroughAuthMode.PUBLIC and wildcard_path not in LiteLLMRoutes.openai_routes.value:
LiteLLMRoutes.openai_routes.value.append(wildcard_path)
InitPassThroughEndpointHelpers.add_subpath_route(
app=app,
path=path,
Expand Down Expand Up @@ -3459,6 +3454,9 @@ async def _get_pass_through_endpoints_from_db(
except Exception:
return []

if response.source == "config":
return []

pass_through_endpoint_data: Final = _config_field_endpoints(response)
if pass_through_endpoint_data is None:
return []
Expand Down
4 changes: 4 additions & 0 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,9 @@ def generate_feedback_box():
from litellm.proxy.openai_files_endpoints.files_endpoints import (
set_files_config,
)
from litellm.proxy.pass_through_endpoints.common_utils import (
warn_pass_through_entries_without_auth,
)
from litellm.proxy.pass_through_endpoints.openai_passthrough_endpoints import (
router as openai_passthrough_router,
)
Expand Down Expand Up @@ -5135,6 +5138,7 @@ def _load_yaml_settings_stores(self, config: Mapping[str, object]) -> None:
if isinstance(yaml_endpoints, list)
else None
)
warn_pass_through_entries_without_auth(config_passthrough_endpoints or ())

def _config_with_resolved_settings(self, config: Mapping[str, object]) -> dict[str, object]:
return { # mutable-ok: get_config preserves the mutable mapping contract used by existing loaders
Expand Down
18 changes: 18 additions & 0 deletions litellm/types/passthrough_endpoints/pass_through_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from typing import Final

from pydantic import TypeAdapter, ValidationError
from typing_extensions import TypedDict

# Request.state key for programmatic pass-through callers (e.g. Bedrock proxy) that attach
Expand Down Expand Up @@ -32,6 +33,23 @@ class EndpointType(str, Enum):
GENERIC = "generic"


class PassThroughAuthMode(str, Enum):
PUBLIC = "public"
ANY_KEY = "any_key"
GRANTED_KEYS = "granted_keys"


_AUTH_FLAG: Final = TypeAdapter(bool)


def pass_through_auth_mode(auth: object) -> PassThroughAuthMode:
try:
enforced: Final = _AUTH_FLAG.validate_python(auth.strip() if isinstance(auth, str) else auth)
except ValidationError:
return PassThroughAuthMode.ANY_KEY
return PassThroughAuthMode.GRANTED_KEYS if enforced else PassThroughAuthMode.PUBLIC


class PassthroughStandardLoggingPayload(TypedDict, total=False):
"""
Standard logging payload for all pass through endpoints
Expand Down
Loading
Loading