Merge pull request #26827 from stuxf/fix/passthrough-auth-default
chore(passthrough): default auth=True and drop enterprise gate on the safe option
This commit is contained in:
commit
71d5015975
@ -2161,8 +2161,8 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase):
|
||||
description="The USD cost per request to the target endpoint. This is used to calculate the cost of the request to the target endpoint.",
|
||||
)
|
||||
auth: bool = Field(
|
||||
default=False,
|
||||
description="Whether authentication is required for the pass-through endpoint. If True, requests to the endpoint will require a valid LiteLLM API key.",
|
||||
default=True,
|
||||
description="Whether authentication is required for the pass-through endpoint. Defaults to True so a pass-through silently created without an explicit value still requires a valid LiteLLM API key — set to False only if the endpoint is meant to be a public forwarder (e.g. an unauthenticated webhook target).",
|
||||
)
|
||||
guardrails: Optional[PassThroughGuardrailsConfig] = Field(
|
||||
default=None,
|
||||
|
||||
@ -473,7 +473,12 @@ 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
|
||||
if endpoint.get("auth") is not True:
|
||||
# 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
|
||||
|
||||
@ -41,7 +41,6 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
from litellm.passthrough import BasePassthroughUtils
|
||||
from litellm.proxy._types import (
|
||||
CommonProxyErrors,
|
||||
ConfigFieldInfo,
|
||||
ConfigFieldUpdate,
|
||||
LiteLLMRoutes,
|
||||
@ -2325,12 +2324,14 @@ async def _register_pass_through_endpoint(
|
||||
dependencies = None
|
||||
|
||||
if auth is not None and str(auth).lower() == "true":
|
||||
if premium_user is not True:
|
||||
raise ValueError(
|
||||
"Error Setting Authentication on Pass Through Endpoint: {}".format(
|
||||
CommonProxyErrors.not_premium_user.value
|
||||
)
|
||||
)
|
||||
# Authentication on a pass-through endpoint used to be enterprise-
|
||||
# only — which left the OSS tier with no safe configuration: the
|
||||
# default was ``auth=False`` (unauthenticated forwarder) and the
|
||||
# safe ``auth=True`` raised at startup unless the operator had a
|
||||
# license. The default is now ``True`` (safe-by-default), and
|
||||
# turning it on no longer requires a license: an unauthenticated
|
||||
# forwarder is a deployment choice the operator should be allowed
|
||||
# to make explicitly, but the safe option must always be free.
|
||||
dependencies = [Depends(user_api_key_auth)]
|
||||
if path not in LiteLLMRoutes.openai_routes.value:
|
||||
LiteLLMRoutes.openai_routes.value.append(path)
|
||||
|
||||
@ -0,0 +1,136 @@
|
||||
"""
|
||||
Regression tests for the pass-through endpoint auth-default fix
|
||||
(GHSA-7h34-mmrh-6g58).
|
||||
|
||||
Two failures the fix closes:
|
||||
|
||||
1. ``PassThroughGenericEndpoint.auth`` defaulted to ``False`` — an
|
||||
admin who added a pass-through to ``general_settings`` without
|
||||
explicitly setting ``auth: true`` shipped an unauthenticated
|
||||
forwarder.
|
||||
2. Setting ``auth: true`` was rejected at startup unless the operator
|
||||
had a LiteLLM Enterprise license, leaving OSS deployments with no
|
||||
safe configuration.
|
||||
|
||||
The fix flips the default to ``True`` (safe-by-default) and removes
|
||||
the enterprise gate so OSS operators can register an authenticated
|
||||
pass-through. The runtime check in ``user_api_key_auth.py`` also now
|
||||
defaults to ``True`` so a config dict (raw, not Pydantic) without an
|
||||
``auth`` key still requires authentication.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../.."))
|
||||
|
||||
from litellm.proxy._types import PassThroughGenericEndpoint
|
||||
from litellm.proxy.auth.user_api_key_auth import (
|
||||
check_api_key_for_custom_headers_or_pass_through_endpoints,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
_register_pass_through_endpoint,
|
||||
)
|
||||
|
||||
|
||||
def test_passthrough_auth_defaults_to_true():
|
||||
# Regression: an admin who configures a pass-through without setting
|
||||
# auth explicitly used to ship an unauthenticated forwarder. The
|
||||
# default is now safe.
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/canary-forwarder",
|
||||
target="https://postman-echo.com/get",
|
||||
)
|
||||
assert endpoint.auth is True
|
||||
|
||||
|
||||
def test_passthrough_auth_can_still_be_explicitly_disabled():
|
||||
# Operators who genuinely need an unauthenticated forwarder (e.g.
|
||||
# public webhook receiver) can opt in explicitly.
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/public-webhook",
|
||||
target="https://example.com/webhook",
|
||||
auth=False,
|
||||
)
|
||||
assert endpoint.auth is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_passthrough_with_auth_true_works_for_oss(monkeypatch):
|
||||
# Regression: setting ``auth: true`` used to raise at startup
|
||||
# unless ``premium_user`` was True, leaving OSS with no safe
|
||||
# configuration.
|
||||
app = MagicMock(spec=FastAPI)
|
||||
visited: set = set()
|
||||
|
||||
endpoint = PassThroughGenericEndpoint(
|
||||
path="/forwarder",
|
||||
target="https://example.com",
|
||||
auth=True,
|
||||
)
|
||||
|
||||
# Should not raise; OSS premium_user=False is allowed to use auth=True.
|
||||
await _register_pass_through_endpoint(
|
||||
endpoint=endpoint,
|
||||
app=app,
|
||||
premium_user=False,
|
||||
visited_endpoints=visited,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_check_treats_missing_auth_key_as_authenticated():
|
||||
# The runtime dispatch in user_api_key_auth pulls
|
||||
# pass_through_endpoints from general_settings as raw dicts (the
|
||||
# Pydantic default never applies). A dict without an ``auth`` key
|
||||
# must default to "authenticated" — without this, the previous
|
||||
# behaviour (``endpoint.get("auth") is not True`` -> True -> empty
|
||||
# auth) ships an unauthenticated forwarder.
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
raw_endpoint_no_auth_key = {
|
||||
"path": "/forwarder",
|
||||
"target": "https://example.com",
|
||||
# ``auth`` deliberately omitted
|
||||
}
|
||||
|
||||
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
|
||||
request=request,
|
||||
route="/forwarder",
|
||||
pass_through_endpoints=[raw_endpoint_no_auth_key],
|
||||
api_key="sk-1234",
|
||||
)
|
||||
|
||||
# Result is the api_key string (auth is REQUIRED for this endpoint
|
||||
# — flow continues to normal key validation), NOT an empty
|
||||
# ``UserAPIKeyAuth()`` (which was the unauthenticated-forwarder
|
||||
# bug).
|
||||
assert result == "sk-1234"
|
||||
|
||||
|
||||
@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 = {
|
||||
"path": "/public-webhook",
|
||||
"target": "https://example.com",
|
||||
"auth": False,
|
||||
}
|
||||
|
||||
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
|
||||
request=request,
|
||||
route="/public-webhook",
|
||||
pass_through_endpoints=[raw_endpoint_auth_false],
|
||||
api_key="",
|
||||
)
|
||||
|
||||
assert isinstance(result, UserAPIKeyAuth)
|
||||
Loading…
Reference in New Issue
Block a user