mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
chore(proxy): default sensitive routes to auth
This commit is contained in:
parent
b31182f62a
commit
35bbca60b0
9 changed files with 83 additions and 27 deletions
|
|
@ -166,7 +166,7 @@ langfuse_default_tags: Optional[List[str]] = None
|
||||||
langsmith_batch_size: Optional[int] = None
|
langsmith_batch_size: Optional[int] = None
|
||||||
prometheus_initialize_budget_metrics: Optional[bool] = False
|
prometheus_initialize_budget_metrics: Optional[bool] = False
|
||||||
prometheus_latency_buckets: Optional[List[float]] = None
|
prometheus_latency_buckets: Optional[List[float]] = None
|
||||||
require_auth_for_metrics_endpoint: Optional[bool] = False
|
require_auth_for_metrics_endpoint: Optional[bool] = True
|
||||||
argilla_batch_size: Optional[int] = None
|
argilla_batch_size: Optional[int] = None
|
||||||
datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload.
|
datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload.
|
||||||
gcs_pub_sub_use_v1: Optional[bool] = (
|
gcs_pub_sub_use_v1: Optional[bool] = (
|
||||||
|
|
|
||||||
|
|
@ -613,13 +613,8 @@ class LiteLLMRoutes(enum.Enum):
|
||||||
"/health/liveness",
|
"/health/liveness",
|
||||||
"/test",
|
"/test",
|
||||||
"/config/yaml",
|
"/config/yaml",
|
||||||
"/metrics",
|
|
||||||
"/litellm/.well-known/litellm-ui-config",
|
"/litellm/.well-known/litellm-ui-config",
|
||||||
"/.well-known/litellm-ui-config",
|
"/.well-known/litellm-ui-config",
|
||||||
"/public/model_hub",
|
|
||||||
"/public/agent_hub",
|
|
||||||
"/public/mcp_hub",
|
|
||||||
"/public/skill_hub",
|
|
||||||
"/public/litellm_model_cost_map",
|
"/public/litellm_model_cost_map",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,12 @@ def _route_requires_auth_despite_public(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
normalized_route = _normalize_public_auth_route(route)
|
normalized_route = _normalize_public_auth_route(route)
|
||||||
if normalized_route == "/metrics":
|
if normalized_route == "/metrics":
|
||||||
return litellm.require_auth_for_metrics_endpoint is True
|
return litellm.require_auth_for_metrics_endpoint is not False
|
||||||
|
|
||||||
if normalized_route in _PUBLIC_AI_HUB_ROUTES:
|
if normalized_route in _PUBLIC_AI_HUB_ROUTES:
|
||||||
return (general_settings or {}).get("require_auth_for_public_ai_hub") is True
|
return (general_settings or {}).get(
|
||||||
|
"require_auth_for_public_ai_hub", True
|
||||||
|
) is True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -1671,7 +1673,7 @@ async def _run_centralized_common_checks(
|
||||||
user_custom_auth,
|
user_custom_auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Public routes (e.g. /health/liveness, /metrics) are exempt from
|
# Public routes (e.g. /health/liveness) are exempt from
|
||||||
# auth in the builder — the wrapper must not retroactively apply
|
# auth in the builder — the wrapper must not retroactively apply
|
||||||
# authz on top, or k8s readiness probes and other unauthenticated
|
# authz on top, or k8s readiness probes and other unauthenticated
|
||||||
# callers get 401.
|
# callers get 401.
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ class PrometheusAuthMiddleware:
|
||||||
"""
|
"""
|
||||||
Middleware to authenticate requests to the metrics endpoint.
|
Middleware to authenticate requests to the metrics endpoint.
|
||||||
|
|
||||||
By default, auth is not run on the metrics endpoint.
|
By default, auth is run on the metrics endpoint.
|
||||||
|
|
||||||
Enabled by setting the following in proxy_config.yaml:
|
To allow unauthenticated metrics in proxy_config.yaml:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
require_auth_for_metrics_endpoint: true
|
require_auth_for_metrics_endpoint: false
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -38,8 +38,8 @@ class PrometheusAuthMiddleware:
|
||||||
await self.app(scope, receive, send)
|
await self.app(scope, receive, send)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only run auth if configured to do so
|
# Run auth by default; allow legacy public metrics only when explicitly disabled.
|
||||||
if litellm.require_auth_for_metrics_endpoint is True:
|
if litellm.require_auth_for_metrics_endpoint is not False:
|
||||||
# Construct Request only when auth is actually needed
|
# Construct Request only when auth is actually needed
|
||||||
request = Request(scope, receive)
|
request = Request(scope, receive)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ router = APIRouter()
|
||||||
async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth:
|
async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth:
|
||||||
from litellm.proxy.proxy_server import general_settings
|
from litellm.proxy.proxy_server import general_settings
|
||||||
|
|
||||||
if (general_settings or {}).get("require_auth_for_public_ai_hub") is True:
|
if (general_settings or {}).get("require_auth_for_public_ai_hub", True) is True:
|
||||||
return await user_api_key_auth(
|
return await user_api_key_auth(
|
||||||
request=request,
|
request=request,
|
||||||
api_key=request.headers.get(SpecialHeaders.openai_authorization.value)
|
api_key=request.headers.get(SpecialHeaders.openai_authorization.value)
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ class UISettings(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
require_auth_for_public_ai_hub: bool = Field(
|
require_auth_for_public_ai_hub: bool = Field(
|
||||||
default=False,
|
default=True,
|
||||||
description="If true, requires authentication for accessing the public AI Hub.",
|
description="If true, requires authentication for accessing the public AI Hub.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ def test_route_requires_auth_despite_public_for_metrics(monkeypatch):
|
||||||
def test_route_requires_auth_despite_public_for_public_ai_hub():
|
def test_route_requires_auth_despite_public_for_public_ai_hub():
|
||||||
settings = {"require_auth_for_public_ai_hub": True}
|
settings = {"require_auth_for_public_ai_hub": True}
|
||||||
|
|
||||||
|
assert _route_requires_auth_despite_public("/public/model_hub", {}) is True
|
||||||
assert _route_requires_auth_despite_public("/public/model_hub", settings) is True
|
assert _route_requires_auth_despite_public("/public/model_hub", settings) is True
|
||||||
assert _route_requires_auth_despite_public("/public/model_hub/", settings) is True
|
assert _route_requires_auth_despite_public("/public/model_hub/", settings) is True
|
||||||
assert (
|
assert (
|
||||||
|
|
@ -74,7 +75,12 @@ def test_route_requires_auth_despite_public_for_public_ai_hub():
|
||||||
assert _route_requires_auth_despite_public("/public/mcp_hub", settings) is True
|
assert _route_requires_auth_despite_public("/public/mcp_hub", settings) is True
|
||||||
assert _route_requires_auth_despite_public("/public/skill_hub", settings) is True
|
assert _route_requires_auth_despite_public("/public/skill_hub", settings) is True
|
||||||
|
|
||||||
assert _route_requires_auth_despite_public("/public/model_hub", {}) is False
|
assert (
|
||||||
|
_route_requires_auth_despite_public(
|
||||||
|
"/public/model_hub", {"require_auth_for_public_ai_hub": False}
|
||||||
|
)
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ def test_valid_auth_metrics(app_with_middleware, monkeypatch):
|
||||||
Test that a request to /metrics (and /metrics/) with valid auth headers passes.
|
Test that a request to /metrics (and /metrics/) with valid auth headers passes.
|
||||||
"""
|
"""
|
||||||
# Enable auth on metrics endpoints.
|
# Enable auth on metrics endpoints.
|
||||||
litellm.require_auth_for_metrics_endpoint = True
|
monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
|
||||||
# Patch the auth function to simulate a valid authentication.
|
# Patch the auth function to simulate a valid authentication.
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
|
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
|
||||||
|
|
@ -92,7 +92,7 @@ def test_invalid_auth_metrics(app_with_middleware, monkeypatch):
|
||||||
"""
|
"""
|
||||||
Test that a request to /metrics with invalid auth headers fails with a 401.
|
Test that a request to /metrics with invalid auth headers fails with a 401.
|
||||||
"""
|
"""
|
||||||
litellm.require_auth_for_metrics_endpoint = True
|
monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
|
||||||
# Patch the auth function to simulate a failed authentication.
|
# Patch the auth function to simulate a failed authentication.
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
|
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
|
||||||
|
|
@ -126,12 +126,29 @@ def test_metrics_auth_uses_real_auth_when_route_is_public(
|
||||||
assert "Unauthorized access to metrics endpoint" in response.text
|
assert "Unauthorized access to metrics endpoint" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_auth_is_required_by_default(app_with_middleware, monkeypatch):
|
||||||
|
"""
|
||||||
|
Metrics should require auth unless explicitly configured as public.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
|
||||||
|
fake_invalid_auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TestClient(app_with_middleware)
|
||||||
|
|
||||||
|
response = client.get("/metrics")
|
||||||
|
|
||||||
|
assert response.status_code == 401, response.text
|
||||||
|
assert "Unauthorized access to metrics endpoint" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch):
|
def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch):
|
||||||
"""
|
"""
|
||||||
Test that when require_auth_for_metrics_endpoint is False, requests to /metrics
|
Test that when require_auth_for_metrics_endpoint is False, requests to /metrics
|
||||||
bypass the auth check.
|
bypass the auth check.
|
||||||
"""
|
"""
|
||||||
litellm.require_auth_for_metrics_endpoint = False
|
monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", False)
|
||||||
|
|
||||||
# To ensure auth is not run, patch the auth function with one that will raise if called.
|
# To ensure auth is not run, patch the auth function with one that will raise if called.
|
||||||
def should_not_be_called(*args, **kwargs):
|
def should_not_be_called(*args, **kwargs):
|
||||||
|
|
@ -148,11 +165,11 @@ def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch):
|
||||||
assert response.json() == {"msg": "metrics OK"}
|
assert response.json() == {"msg": "metrics OK"}
|
||||||
|
|
||||||
|
|
||||||
def test_non_metrics_requests_pass_through(app_with_middleware):
|
def test_non_metrics_requests_pass_through(app_with_middleware, monkeypatch):
|
||||||
"""
|
"""
|
||||||
Test that non-metrics endpoints pass through the middleware unaffected.
|
Test that non-metrics endpoints pass through the middleware unaffected.
|
||||||
"""
|
"""
|
||||||
litellm.require_auth_for_metrics_endpoint = True
|
monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
|
||||||
|
|
||||||
client = TestClient(app_with_middleware)
|
client = TestClient(app_with_middleware)
|
||||||
|
|
||||||
|
|
@ -170,7 +187,7 @@ def test_non_metrics_requests_dont_trigger_auth(app_with_middleware, monkeypatch
|
||||||
Test that non-metrics requests never trigger auth, even when auth is enabled
|
Test that non-metrics requests never trigger auth, even when auth is enabled
|
||||||
and the auth function would reject the request.
|
and the auth function would reject the request.
|
||||||
"""
|
"""
|
||||||
litellm.require_auth_for_metrics_endpoint = True
|
monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
|
||||||
|
|
||||||
def should_not_be_called(*args, **kwargs):
|
def should_not_be_called(*args, **kwargs):
|
||||||
raise Exception("Auth should not be called for non-metrics requests")
|
raise Exception("Auth should not be called for non-metrics requests")
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from fastapi.testclient import TestClient
|
||||||
from starlette.datastructures import URL
|
from starlette.datastructures import URL
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from litellm.proxy._types import ProxyException
|
from litellm.proxy._types import LitellmUserRoles, ProxyException
|
||||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||||
from litellm.proxy.public_endpoints import router
|
from litellm.proxy.public_endpoints import router
|
||||||
from litellm.proxy.public_endpoints.public_endpoints import (
|
from litellm.proxy.public_endpoints.public_endpoints import (
|
||||||
|
|
@ -97,7 +97,7 @@ def test_get_litellm_model_cost_map_returns_cost_map():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_public_ai_hub_info_stays_public_by_default(monkeypatch):
|
def test_public_ai_hub_info_requires_auth_by_default(monkeypatch):
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
@ -105,16 +105,31 @@ def test_public_ai_hub_info_stays_public_by_default(monkeypatch):
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
|
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
|
||||||
|
|
||||||
|
with pytest.raises(ProxyException):
|
||||||
|
client.get("/public/model_hub/info")
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_ai_hub_info_can_be_explicitly_public(monkeypatch):
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"litellm.proxy.proxy_server.general_settings",
|
||||||
|
{"require_auth_for_public_ai_hub": False},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
|
||||||
|
|
||||||
response = client.get("/public/model_hub/info")
|
response = client.get("/public/model_hub/info")
|
||||||
|
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_public_ai_hub_info_requires_auth_when_configured(monkeypatch):
|
async def test_public_ai_hub_info_requires_auth_by_default_dependency(monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"litellm.proxy.proxy_server.general_settings",
|
"litellm.proxy.proxy_server.general_settings",
|
||||||
{"require_auth_for_public_ai_hub": True},
|
{},
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
|
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
|
||||||
request = Request(
|
request = Request(
|
||||||
|
|
@ -131,6 +146,27 @@ async def test_public_ai_hub_info_requires_auth_when_configured(monkeypatch):
|
||||||
await public_ai_hub_auth_dependency(request)
|
await public_ai_hub_auth_dependency(request)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_public_ai_hub_info_skips_auth_when_explicitly_disabled(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"litellm.proxy.proxy_server.general_settings",
|
||||||
|
{"require_auth_for_public_ai_hub": False},
|
||||||
|
)
|
||||||
|
request = Request(
|
||||||
|
scope={
|
||||||
|
"type": "http",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/public/model_hub/info",
|
||||||
|
"headers": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request._url = URL(url="/public/model_hub/info")
|
||||||
|
|
||||||
|
auth = await public_ai_hub_auth_dependency(request)
|
||||||
|
|
||||||
|
assert auth.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
|
||||||
|
|
||||||
|
|
||||||
def test_watsonx_provider_fields():
|
def test_watsonx_provider_fields():
|
||||||
"""Test that Watsonx provider has all required credential fields including multiple auth options."""
|
"""Test that Watsonx provider has all required credential fields including multiple auth options."""
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue