feat(auth): resolve caller identity once into a Principal at the auth seam - #30887
Conversation
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR lands Phase 0 of a "Caller Identity: Resolve Once" refactor: a new
Confidence Score: 5/5Safe to merge — the change is fully additive, projection failures are explicitly non-fatal, and the existing auth flow is structurally unchanged. All call sites that previously called
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/resolvers/models.py | New frozen Pydantic Principal value type; identity-only fields, no policy/budget state. source_key carrier is correctly excluded from serialization and repr. |
| litellm/proxy/auth/resolvers/store.py | New IdentityStore with cache-first key resolution. Behavior mirrors original get_key_object (raises before cache when prisma_client=None). _principal_from_key is a private static used externally. |
| litellm/proxy/auth/resolvers/exceptions.py | Typed exception hierarchy; KeyNotFoundError dual-inherits from IdentityResolutionError and ProxyException, preserving the existing 401 contract. |
| litellm/proxy/auth/user_api_key_auth.py | Five get_key_object call sites replaced with inline IdentityStore construction; _resolve_request_principal stamps request.state.principal with warning-level logging on failure. Private static _principal_from_key called from module scope. |
| litellm/proxy/auth/roles.py | New Role / TeamRole enums with map_role helper. Only 3 of 7 LiteLLM user roles are mapped; internal_user and others yield empty Principal.roles. |
| litellm/proxy/auth/network.py | New NetworkContext / TrustedProxyConfig models and resolve_client_ip (right-to-left XFF walk). CIDR parsing happens per-call; already flagged in prior review threads. |
| litellm/proxy/auth/trusted_proxy_utils.py | Refactored to delegate IP-range logic to network.py; get_trusted_proxy_cidrs helper extracted for use at the auth seam. |
| litellm/proxy/auth/auth_method.py | New AuthMethod enum with 7 entries. Only API_KEY and BEARER_JWT are currently projected at the seam (per previous thread discussion). |
| tests/test_litellm/proxy/auth/test_resolvers_seam.py | New unit tests for the auth seam: identity projection, non-anonymous credential_ref, XFF behavior, JWT detection. No real network calls. |
| tests/test_litellm/proxy/auth/test_resolvers_store.py | Store tests with a _FakeCache stub; covers cache-hit projection, key_from_principal recovery, and no-DB error. Async tests run via asyncio_mode = "auto" in pyproject.toml. |
| tests/proxy_unit_tests/test_jwt_key_mapping.py | Mocks repointed from get_key_object to IdentityStore._resolve_key. Race-condition test now only asserts called_once_with("winner_token_hash") — construction-time args (prisma_client, cache) no longer verified. |
| scripts/ruff_strict_gate.py | Refactored: gather/report functions inlined into cmd_check; GateInputs named tuple removed; _temp_worktree context manager replaced with inline try/finally. Logic unchanged. |
Reviews (4): Last reviewed commit: "feat(auth): resolve caller identity once..." | Re-trigger Greptile
8e876c7 to
ffab361
Compare
|
@greptileai re-review please; updated HEAD: get_key_object resolution moved into DbIdentityStore, and the X-Forwarded-For/CIDR primitives consolidated into resolvers/network.py |
78cebc9 to
83f8e6a
Compare
83f8e6a to
09b5607
Compare
… seam 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.
09b5607 to
1639988
Compare
… seam (BerriAI#30887) 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.
Relevant issues
Implements Phase 0 of the "Caller Identity: Resolve Once, Consume Everywhere" design (internal Notion). Supersedes the broad draft in #30171 by landing only the identity foundation, relocated under
litellm/proxy/auth/rather than a parallelauth_v2/.Linear ticket
None
Pre-Submission checklist
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewCI (LiteLLM team)
Link:
Link:
Links:
Type
🆕 New Feature
Changes
This lands the caller-identity foundation: one typed, identity-only
Principalresolved once at the auth seam and read by reference downstream, instead of identity being re-derived from the 50-field key object or rebuilt from request metadata strings.New package
litellm/proxy/auth/resolvers/, organized by responsibility:Principal: a small frozen value type carrying user / organization / teams / project / end-user / roles / scopes / network, with its sub-models and role mapping. It holds no budget or policy state; those stay on the key object by design.principal_from_key: projects aPrincipaloff an already-resolved key object and issues no lookup, so identity is assembled the same way wherever it is read.DbIdentityStore: the single chokepoint for key resolution. It owns thecombined_viewlookup via the cache-firstget_key_object.user_api_key_authnow resolves keys through the store instead of callingget_key_objectdirectly, so there is one place that does the lookup. The store returns the key object, which still flows unchanged for budget, rate-limit, and policy.At the seam,
user_api_key_authprojects one per-requestPrincipaloff the resolved key object and stamps the request network context onto it once; X-Forwarded-For is trusted only whentrusted_proxy_rangesis configured, reusing the existingtrusted_proxy_utilsrather than a second parser. It is attached torequest.state.principalfor the consumers that later phases add. The projection is additive and defensive: a failure never rejects an already-authenticated request, and any future reader must treat a missing principal as deny. ThePrincipalis always identifiable (acredential_refand a stablesubjectare taken off the token), so it is never anonymous.This is additive and changes no behavior today; it is the foundation the spend-attribution and authorization phases build on. Deliberately not included, to keep scope tight: the authenticators, RBAC/ABAC, SCIM, and session modules from the draft, and the downstream budget-object collapse in
common_checks(that one is a separate, canary-gated change because those fetches carry budget state, not identity).Note on the design's "remove redundant identity passes": verifying against a live proxy showed current litellm already resolves the team's org onto the token and already carries
team_aliasviacombined_view, so the org / team-alias re-resolution the design targets is already handled upstream. There was no safe identity-only redundancy left to delete, so this PR does not add a no-op "fix" for it.Screenshots / Proof of Fix
Run against a live proxy on
localhost:4000backed by Postgres, hitting the real Anthropic API. The build routes all key resolution throughDbIdentityStoreand projects aPrincipalat the seam.Setup: an org, a team in that org, and a team-scoped key.
Valid key resolves through the resolver and returns a real completion:
Invalid key is rejected, master key still reaches admin routes:
Spend attribution is intact through the resolver path. The chat-completion rows carry the right ids:
Unit coverage: the new
tests/test_litellm/proxy/auth/test_resolvers_*.pycover thePrincipalmodel, the seam projection (identity off the key object, network stamping, non-anonymous credential_ref/subject), the XFF parser, and the store. The existinguser_api_key_auth/auth_checks/handle_jwtsuites stay green (344 passing), and the key-lookup mocks were repointed to the resolver's delegate.Auth span comparison
Created a virtual key with access to openai model. Sent POST request. Auth span before and after should be the same.
Before changes
First chat completions


Second chat completions
After changes
First chat completions

Second chat completions
