mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +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.
95 lines
2.5 KiB
Python
95 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from litellm.proxy.auth.auth_method import AuthMethod
|
|
from litellm.proxy.auth.resolvers.models import (
|
|
EndUserIdentity,
|
|
Principal,
|
|
PrincipalType,
|
|
ProjectIdentity,
|
|
TeamIdentity,
|
|
UserIdentity,
|
|
)
|
|
from litellm.proxy.auth.roles import Role, TeamRole
|
|
|
|
|
|
def _principal() -> Principal:
|
|
return Principal(
|
|
principal_type=PrincipalType.HUMAN,
|
|
subject="u1",
|
|
auth_method=AuthMethod.OIDC,
|
|
)
|
|
|
|
|
|
def test_principal_is_frozen():
|
|
principal = _principal()
|
|
with pytest.raises(ValidationError):
|
|
principal.subject = "mutated"
|
|
|
|
|
|
def test_principal_defaults_are_independent_instances():
|
|
a = _principal()
|
|
b = _principal()
|
|
assert a.teams == [] and a.scopes == [] and a.audience == []
|
|
assert a.teams is not b.teams
|
|
|
|
|
|
def test_principal_requires_identity_core_fields():
|
|
with pytest.raises(ValidationError):
|
|
Principal(subject="u1") # missing principal_type + auth_method
|
|
|
|
|
|
def test_principal_roles_are_validated_against_role_enum():
|
|
principal = Principal(
|
|
principal_type=PrincipalType.HUMAN,
|
|
subject="u1",
|
|
auth_method=AuthMethod.OIDC,
|
|
roles=["org_admin"],
|
|
)
|
|
assert principal.roles == [Role.ORG_ADMIN]
|
|
assert isinstance(principal.roles[0], Role)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Principal(
|
|
principal_type=PrincipalType.HUMAN,
|
|
subject="u1",
|
|
auth_method=AuthMethod.OIDC,
|
|
roles=["not_a_real_role"],
|
|
)
|
|
|
|
|
|
def test_principal_default_network_and_collections():
|
|
principal = Principal(
|
|
principal_type=PrincipalType.SERVICE_ACCOUNT,
|
|
subject="svc",
|
|
auth_method=AuthMethod.MUTUAL_TLS,
|
|
)
|
|
assert principal.teams == []
|
|
assert principal.scopes == []
|
|
assert principal.project is None
|
|
assert principal.end_user is None
|
|
assert principal.network.client_ip is None
|
|
assert principal.network.via_trusted_proxy is False
|
|
|
|
|
|
def test_team_identity_defaults_to_member_role():
|
|
team = TeamIdentity(id="g1")
|
|
assert team.role == TeamRole.MEMBER
|
|
|
|
|
|
def test_user_identity_optional_fields_default_none():
|
|
user = UserIdentity(id="u1")
|
|
assert user.email is None
|
|
assert user.external_id is None
|
|
|
|
|
|
def test_project_identity_name_is_optional():
|
|
assert ProjectIdentity(id="p1").name is None
|
|
assert ProjectIdentity(id="p1", name="Acme").name == "Acme"
|
|
|
|
|
|
def test_end_user_identity_requires_id():
|
|
with pytest.raises(ValidationError):
|
|
EndUserIdentity()
|