mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Introduce a single, typed caller identity that is resolved once at the auth boundary and read by reference downstream, instead of being re-derived from a 50-field key object or rebuilt from request metadata. What this adds (litellm/proxy/auth/resolvers/), organized by responsibility: - Principal: a small, frozen, identity-only value type (user / organization / teams / project / end-user / roles / scopes / network), with its sub-models and the role mapping. No budget or policy state; those stay on the key object. - DbIdentityStore: the auth flow's resolver, owning both halves of resolving a caller. resolve_key does the one combined_view lookup (cache, then DB via the shared lower-level helpers, then write-back) and returns the key object, which still flows for budget / rate-limit / policy unchanged. principal_from_key projects the identity slice of that key object into a Principal, issuing no lookup. user_api_key_auth resolves every key through the store rather than calling get_key_object directly; auth_checks.get_key_object stays as the legacy entrypoint for its other callers until they migrate. - network: the X-Forwarded-For / trusted-proxy CIDR primitives live here in one place. trusted_proxy_utils now imports them rather than keeping a second copy. At the seam, user_api_key_auth projects one per-request Principal off the resolved key object and stamps the request network context onto it once (X-Forwarded-For is trusted only when trusted_proxy_ranges is configured). It is attached to request.state.principal for the downstream consumers later phases add. The projection is additive and defensive: a failure never rejects an already-authenticated request, and a missing principal must be treated as deny by any future reader. The Principal is always identifiable (credential_ref and a stable subject off the token), never anonymous. This is additive and changes no behavior today; it is the identity foundation the spend-attribution and authorization phases build on.
104 lines
3 KiB
Python
104 lines
3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from fastapi import Request
|
|
|
|
from litellm.proxy.auth.network import (
|
|
TrustedProxyConfig,
|
|
resolve_client_ip,
|
|
resolve_network_context,
|
|
)
|
|
|
|
TRUSTED = TrustedProxyConfig(use_forwarded_for=True, trusted_proxy_cidrs=["10.0.0.0/8"])
|
|
|
|
|
|
def make_request(
|
|
*,
|
|
headers: Optional[Dict[str, str]] = None,
|
|
client: Optional[Tuple[str, int]] = ("203.0.113.7", 5555),
|
|
) -> Request:
|
|
raw_headers: List[Tuple[bytes, bytes]] = [
|
|
(key.lower().encode(), value.encode()) for key, value in (headers or {}).items()
|
|
]
|
|
scope: Dict[str, Any] = {
|
|
"type": "http",
|
|
"http_version": "1.1",
|
|
"method": "GET",
|
|
"path": "/",
|
|
"raw_path": b"/",
|
|
"query_string": b"",
|
|
"headers": raw_headers,
|
|
"client": client,
|
|
"server": ("testserver", 80),
|
|
"scheme": "http",
|
|
}
|
|
return Request(scope)
|
|
|
|
|
|
def test_xff_ignored_when_forwarding_disabled():
|
|
config = TrustedProxyConfig(
|
|
use_forwarded_for=False, trusted_proxy_cidrs=["10.0.0.0/8"]
|
|
)
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "203.0.113.9"}, client=("10.0.0.1", 1)
|
|
)
|
|
ip, via_proxy = resolve_client_ip(request, config)
|
|
assert ip == "10.0.0.1"
|
|
assert via_proxy is False
|
|
|
|
|
|
def test_xff_honored_from_trusted_peer():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "203.0.113.9, 10.0.0.5"}, client=("10.0.0.1", 1)
|
|
)
|
|
ip, via_proxy = resolve_client_ip(request, TRUSTED)
|
|
assert ip == "203.0.113.9"
|
|
assert via_proxy is True
|
|
|
|
|
|
def test_spoofed_xff_from_untrusted_peer_is_ignored():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "203.0.113.9"}, client=("8.8.8.8", 1)
|
|
)
|
|
ip, via_proxy = resolve_client_ip(request, TRUSTED)
|
|
assert ip == "8.8.8.8"
|
|
assert via_proxy is False
|
|
|
|
|
|
def test_right_to_left_parse_skips_chained_trusted_proxies():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "198.51.100.4, 10.1.1.1, 10.0.0.9"},
|
|
client=("10.0.0.1", 1),
|
|
)
|
|
ip, via_proxy = resolve_client_ip(request, TRUSTED)
|
|
assert ip == "198.51.100.4"
|
|
assert via_proxy is True
|
|
|
|
|
|
def test_all_trusted_hops_fall_back_to_peer():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "10.1.1.1, 10.0.0.9"}, client=("10.0.0.1", 1)
|
|
)
|
|
ip, via_proxy = resolve_client_ip(request, TRUSTED)
|
|
assert ip == "10.0.0.1"
|
|
assert via_proxy is True
|
|
|
|
|
|
def test_invalid_xff_token_is_skipped():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "not-an-ip, 203.0.113.50"}, client=("10.0.0.1", 1)
|
|
)
|
|
ip, _ = resolve_client_ip(request, TRUSTED)
|
|
assert ip == "203.0.113.50"
|
|
|
|
|
|
def test_network_context_captures_host_and_proxy_flag():
|
|
request = make_request(
|
|
headers={"x-forwarded-for": "203.0.113.9", "host": "proxy.litellm.ai"},
|
|
client=("10.0.0.1", 1),
|
|
)
|
|
ctx = resolve_network_context(request, TRUSTED)
|
|
assert ctx.client_ip == "203.0.113.9"
|
|
assert ctx.host == "proxy.litellm.ai"
|
|
assert ctx.via_trusted_proxy is True
|