Commit graph

41067 commits

Author SHA1 Message Date
ryan-crabbe-berri
0d120de785
chore(hooks): enforce Conventional Commits and Conventional Branches (#30174)
* chore(hooks): enforce Conventional Commits and Conventional Branches

Adds opt-in local git hooks plus a CI PR-title check:

- .githooks/commit-msg validates commit subjects against Conventional
  Commits 1.0.0 (feat|fix|docs|style|refactor|perf|test|build|ci|
  chore|revert)(scope)!: subject. Merge/revert/fixup!/squash!/amend!
  messages pass through; --no-verify still works.
- .githooks/pre-push validates branch names against Conventional
  Branches (feature|bugfix|hotfix|release|chore)/desc. Bypasses
  main, litellm_internal_staging, dependabot/*, gh-readonly-queue/*.
  Tag pushes and deletions are skipped.
- scripts/install_git_hooks.sh sets core.hooksPath=.githooks and is
  wired up as 'make install-hooks'. Opt-in — not chained into
  install-dev.
- .github/workflows/conventional-commits.yml validates PR titles via
  amannn/action-semantic-pull-request pinned to v6.1.1's SHA. This is
  the actual gate since squash-merge uses the PR title as the commit
  subject.
- tests/test_litellm/test_git_hooks.py exercises both hooks via
  subprocess for accept / reject / bypass / git-generated-message
  cases.
- CONTRIBUTING.md documents the conventions, the install step, the
  bypass list, and the --no-verify escape hatch.

Resolves LIT-3306

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(hooks): address Greptile review on PR #28703

Resolves two findings from the automated code review:

1. CONTRIBUTING.md: shrink the new Conventional Commits / Branches
   section to a 2-line pointer at docs.litellm.ai. Per the team
   convention, the full documentation lives in the litellm-docs
   repo — see BerriAI/litellm-docs#208 for the companion change that
   adds the section to docs/extras/contributing_code.md.

2. .githooks/commit-msg: tighten the subject regex to also reject an
   uppercase first letter in the description. CI's subjectPattern is
   ^(?![A-Z]).+$ so the previous local hook would accept 'feat: Add
   thing' which would then fail the PR-title check. The local hook is
   now the strictly tighter of the two gates. Test cases extended to
   cover both the new rejection and the digit/symbol-start cases that
   remain allowed.

Resolves LIT-3306

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: trigger ci after branch rename

* fix(ci): rerun pr title check when bypass label changes

amannn/action-semantic-pull-request only honors ignoreLabels if the
workflow retriggers on labeled/unlabeled events; without them a red
check stays red after a maintainer applies the bypass label.

Also point the CONTRIBUTING.md workflow comments at the conventions
section, which now sits above the Development Workflow section.

---------

Co-authored-by: Yassin Kortam <yassinkortam@Yassins-MBP.localdomain>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 10:00:23 -07:00
Yassin Kortam
343909d632 test(auth_v2): cover deep SCIM path under keyMatch and drop stale comment
Add a platform_admin POST /scim/v2/Groups assertion alongside the existing
/scim/v2/Users DELETE so the multi-segment grant is pinned on a second deep
path, and correct the stale keyMatch2 comment to keyMatch.
2026-06-11 09:52:18 -07:00
Yassin Kortam
2fad79b4fe test(auth_v2): pin multi-segment path authorization (keyMatch)
The Casbin object matcher spans path separators now, so a "/*" or "/api/*"
policy covers nested routes:
- a granted role is allowed on a multi-level path (platform_viewer GET
  /api/v1/models, platform_admin POST /api/v1/x/y)
- an ungranted role/verb is still denied across segments (org_viewer and
  GET-only viewers on writes), and the anchored act matcher still rejects a
  superstring verb (GETX)
- an operator CSV object pattern spans segments the same way

Full auth_v2 suite: 178 passing.
2026-06-11 09:51:14 -07:00
Yassin Kortam
e989016c2d fix(auth_v2): match request paths with keyMatch so obj patterns span segments
Use keyMatch instead of keyMatch2 in the Casbin matcher so a "/*" or
"/scim/v2/*" obj pattern unambiguously spans path separators - a require_permission
check on a multi-level route like /api/v1/models now matches the granting policy
rather than risking a 403. keyMatch is the canonical trailing-wildcard route
matcher; the anchored act matcher is unchanged, so a "GET" policy still cannot
grant "GETX".
2026-06-11 09:46:52 -07:00
Mateo Wang
49ca04d8c3
feat(bedrock): aws_bedrock_project_id for bedrock-mantle project / workspace association (#30163)
* feat(bedrock): support aws_bedrock_project_id for bedrock-mantle project association

Adds a litellm_params field to associate bedrock-mantle requests with an
Amazon Bedrock project, sent as the OpenAI-Project header on the
OpenAI-compatible chat and responses paths and as the anthropic-workspace
header on the Anthropic messages paths. This lets a single model entry opt
into a project-scoped data retention mode (e.g. provider_data_share for
Claude Fable 5) while the account-wide setting stays on default.

The param is carried via litellm_params only and is explicitly excluded
from optional_params so it can never leak into a request body.

Fixes #30070

* chore(ui): regenerate schema.d.ts for aws_bedrock_project_id

Generated with npm run gen:api after adding the field to LiteLLM_Params

* fix(proxy): ban client-supplied aws_bedrock_project_id in request bodies

The deployment pins aws_bedrock_project_id so the project's data
retention policy applies to its requests. Without this guard an
authenticated caller could supply the field in the request body and,
since client kwargs win the router merge, run requests under any
project reachable with the deployment's shared AWS credentials.

Adds the field to _BANNED_REQUEST_BODY_PARAMS so it is rejected at the
auth boundary by default while remaining available through the existing
admin opt-ins (allow_client_side_credentials proxy-wide or
configurable_clientside_auth_params per deployment).
2026-06-11 10:01:08 +05:30
Mateo Wang
7a96b3490d
[internal copy of #30137] perf(realtime): eliminate redundant per-frame JSON work on OpenAI realtime relay (#30142)
* perf(realtime): eliminate redundant per-frame JSON work on OpenAI realtime relay

The GA realtime support added in #27110 made backend_to_client_send_messages
parse every backend frame up to three times for beta clients (OpenAI-Beta:
realtime=v1), build a discarded Pydantic object per frame for logging, and
re-serialize even frames that need no translation. For high-frequency
response.output_audio.delta frames carrying multi-KB base64 payloads, that
serialized CPU work on the hottest relay path drove the latency regression
between v1.83.14 and v1.88.1 for gpt-realtime-1.5 and gpt-realtime-2.

This parses each frame once via _parse_backend_event and threads the dict into
_handle_raw_backend_message, store_message, and _translate_event_to_beta;
short-circuits store_message before the Pydantic build for events not in the
logged set; returns the original event unchanged from _translate_event_to_beta
when no rename applies so the raw frame is forwarded without re-serialization;
and only json.dumps when the type is actually renamed.

* fix(realtime): widen store_message type hint to accept plain dict

The parse-once refactor passes the dict produced by _parse_backend_event into
store_message, but the parameter was typed as str | bytes | OpenAIRealtimeEvents
(a union of TypedDicts), which mypy does not consider compatible with a plain
dict. Add dict to the accepted union; the body already handles it.

---------

Co-authored-by: Miguel Armenta <maarmenta92@gmail.com>
2026-06-11 09:56:35 +05:30
ishaan-berri
4a3860df1f
fix: completion_cost AttributeError on streaming Anthropic web_search responses (#26153) (#27346)
* fix: coerce server_tool_use dict to ServerToolUse in Usage.__init__ (#26153)

* fix: coerce server_tool_use to ServerToolUse in stream_chunk_builder (#26153)

* fix: dict/pydantic-tolerant access in tool_call_cost_tracking (#26153)

* fix: dict/pydantic-tolerant access in anthropic cost_calculation (#26153)

* test: assert ServerToolUse type in existing stream_chunk_builder anthropic web search test

* test: regression test for #26153 (stream_chunk_builder server_tool_use type)

* test: dict/pydantic safety for tool_call_cost_tracking helper

* test: dict/pydantic safety for anthropic web_search cost

* refactor: consolidate _get_web_search_requests into shared cost-calc utils

* test(realtime): use gpt-realtime; openai retired gpt-4o-realtime-preview

OpenAI shut down the gpt-4o-realtime-preview family (incl. the undated
alias) on 2026-05-07, causing the live realtime test to fail with a
4000 invalid_request_error.invalid_model close. gpt-realtime is the GA
successor; switch the live-call tests to it, matching the base branch.

* refactor(types): drop redundant server_tool_use coercion in Usage.__init__

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-10 21:20:11 -07:00
Yassin Kortam
a7f9d5d6b9 test(auth_v2): pin the mTLS forwarded-DN gate against XFF spoofing
The forwarded subject-DN trust gate keys on the raw socket peer and prefers a
verified TLS-layer cert:
- an untrusted peer cannot smuggle a forged DN by claiming a trusted address via
  X-Forwarded-For (the gate ignores XFF)
- a verified client cert from the ASGI TLS extension wins over a proxy-forwarded
  DN header

Full auth_v2 suite: 175 passing.
2026-06-10 20:29:03 -07:00
Yassin Kortam
c9e7fd829c fix(auth_v2): prefer verified mTLS cert and confirm forwarded-DN gates on the socket peer
The forwarded subject-DN trust gate already compared the raw transport peer
(request.client.host / ASGI scope client) against trusted_proxy_cidrs, never the
XFF-resolved IP, so an attacker spoofing X-Forwarded-For cannot defeat it. Make
that ordering explicit and stronger: a genuinely verified client certificate from
the ASGI TLS extension is now preferred when present, and the spoofable
forwarded-header path is only consulted as a fallback, still gated on the direct
socket peer.
2026-06-10 20:25:37 -07:00
Yassin Kortam
f158a6c9e0 ci(auth_v2): run auth_v2 tests in a dedicated shard to avoid test_models basename clash
Adding tests/test_litellm/proxy/auth_v2 to the proxy-auth shard collided with proxy/client/test_models.py (both bare test_models, no package __init__), failing collection with an import file mismatch. Run the auth_v2 suite in its own job with the xmlsec1 apt packages instead; the base apt-packages input added earlier is reused.
2026-06-10 20:18:24 -07:00
Yassin Kortam
ac971d7e8b test(auth_v2): pin the role allowlist on the OIDC-login and SAML-SSO paths
The veria review-response fix gates IdP-asserted roles through the same
per-provider allowlist on every role-bearing path, not just JWT bearer tokens.

- rbac: filter_claim_roles (the shared gate) denies a self-asserted role by
  default, filters to the allowlist, and only admits platform roles behind the
  explicit allow_platform_roles flag
- saml: a signed SSO assertion asserting platform_admin yields a session whose
  Principal has no roles by default, and is filtered to the allowlist when set
- oidc: the login callback's identity build (map userinfo -> gate roles ->
  session) denies platform_admin by default and filters to the allowlist

Full auth_v2 suite: 173 passing.
2026-06-10 20:06:04 -07:00
Yassin Kortam
44ac50493e ci(auth_v2): run the proxy auth_v2 suite with xmlsec1 in the proxy-auth shard
The proxy/auth_v2 tests were not picked up by any shard because the proxy-auth path list matched the literal proxy/auth directory, not its sibling. Add the path and install xmlsec1 so the pysaml2 signing tests run rather than skip; the install is gated behind a new optional apt-packages input so the other shards are unchanged.
2026-06-10 20:03:24 -07:00
Yassin Kortam
200f674b94 test(auth_v2): pin the token-claim hardening and provisioning security fixes
Regression coverage for the security review fixes (H1/M1/M3/S7):
- resolver: a deactivated SCIM user (active=False) is rejected 403; claims
  whose keys start with "_" never surface on the Principal
- authenticators: H1 privilege escalation - a self-asserted token role grants
  nothing without a per-provider allowlist, the allowlist filters roles, and
  platform-level roles need an explicit allow_platform_roles gate
- rbac: the Casbin act matcher is anchored, so a "GET" policy does not grant
  "GETX"
- scim: PATCH that targets the read-only id (replace, remove, no-path replace)
  is rejected 400 with the record's id unchanged; unauthenticated/under-scoped
  requests render a SCIM Error body (401/403); /Schemas is a ListResponse
  envelope
- saml: a replayed signed assertion is rejected 401 (single-use), and an
  unsolicited IdP-initiated response is rejected 401 when allow_unsolicited is
  off (default secure)

Full auth_v2 suite: 165 passing.
2026-06-10 19:58:44 -07:00
Yassin Kortam
99efd314f7 fix(auth_v2): enforce the role allowlist on the SAML SSO path too
The per-provider role allowlist + platform-role gate only covered the bearer-JWT
path, so a SAML IdP could still mint platform_admin (or any Role) through its
attribute->roles mapping. Extract the filter into a shared rbac.filter_claim_roles,
add allowed_roles/allow_platform_roles to SAMLConfig (default empty = no roles
from the assertion), and apply it in the ACS before the attributes become claims,
so SSO paths enforce the same role policy as token paths. The JWT path now reuses
the same helper.
2026-06-10 19:52:46 -07:00
Yassin Kortam
fc6d51cfc0 fix(auth_v2): gate OIDC login roles through the provider allowlist
The browser OIDC login/callback path accepted IdP-asserted roles straight into
the session, so a malicious or misconfigured IdP could assert platform_admin
over SSO and have it land in the Principal - the same escalation the bearer
token path already closes. The callback now runs the mapped claims through the
shared _apply_role_policy with the matched provider config before minting the
session, so roles outside allowed_roles are dropped and platform roles require
allow_platform_roles. With the defaults (empty allowlist, platform off) no
IdP-asserted role survives.
2026-06-10 19:52:15 -07:00
Yassin Kortam
341e75ab45 test(auth_v2): repoint to frozen sub-package layout and pin H1 group provisioning
Follow the sub-package split (oidc/saml/scim/*) and the token-claim hardening:
- import config models (OIDCProviderConfig, SAMLConfig) from the top-level
  package and the moved helpers from their concrete sub-modules
  (saml.router, saml.config, oidc.router) so tests are stable against
  __init__ re-export churn
- resolver: a token group claim is no longer authoritative on its own; it
  becomes a TeamIdentity only when it resolves to a provisioned SCIM Group in
  the store (split into provisioned vs not-provisioned cases)

Full auth_v2 suite green (153) and stable across repeated runs.
2026-06-10 19:50:23 -07:00
Yassin Kortam
a9be3d23e0 refactor(auth_v2): drop duplicate SCIM error helper and private re-exports
S7 is already handled self-contained by the SCIM router's route_class (renders
401/403 as a SCIM Error while preserving WWW-Authenticate), so remove the
redundant errors.scim_error_response and its AuthSecurity docstring note. Also
stop re-exporting private underscore helpers from the oidc/saml sub-package
__init__s; the public names (config + build_*_router) remain re-exported and
test code references the concrete modules for internals.
2026-06-10 19:45:14 -07:00
Yassin Kortam
fec8e0a039 fix(auth_v2): complete the freeze batch (re-exports, SCIM error helper, bounded SAML login state)
- Re-export the test/public helpers from the sub-package __init__s so existing
  imports resolve: oidc exposes _provider_key/_user_from_userinfo, saml exposes
  _map_attributes/_metadata_source/_user_from_mapped.
- S7: add errors.scim_error_response(exc) rendering the RFC 7644 SCIM Error schema;
  a host registers it as the exception handler for the SCIM routes (noted on the
  AuthSecurity docstring).
- veria-ai MEDIUM: bound the SAML outstanding-request map with a TTL (300s) and
  max-size eviction, the same treatment as the session store, so unauthenticated
  /auth/saml/login traffic can no longer accumulate login state unbounded.
- Silence a fastapi/starlette generic-Request override quirk on the SCIM route's
  get_route_handler so mypy is clean at the freeze sha.
2026-06-10 19:41:28 -07:00
Sameer Kankute
6068bb7781
fix(proxy): align /v1/model/info with router deployments (#30025)
* fix(proxy): align /v1/model/info with router deployments

Return router model_list entries (including team-scoped models) with team
access metadata instead of wildcard-expanded names from get_complete_model_list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): gate v1 team filter and honor key allowlists

Only apply get_all_team_and_direct_access_models for admin or user-bound
keys, then intersect with key/team model restrictions to avoid empty lists
for service tokens and metadata leaks for restricted keys.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): skip v1 team filter when user row is missing

Require a DB-backed user before applying team-access filtering on
/v1/model/info, and skip the trailing filter in get_all_team_and_direct_access_models
when user context cannot be resolved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert "fix(proxy): skip v1 team filter when user row is missing"

This reverts commit 74e1fbd77a.

* fix(proxy): restore legacy v1 model access filtering

Keep /v1/model/info on key/team allowlists instead of DB team-membership
filtering, while still listing router deployments for team-scoped models.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): drop A2A agent entries from public /v1/model/info list

* fix(proxy): scope team BYOK rows on /v1/model/info to caller's teams

Listing the full router model_list let any authenticated key without
explicit model restrictions enumerate other teams' BYOK deployments
(public name, team_id, api_base) via /v1/model/info. Reuse the existing
_get_caller_byok_team_scope check so non-admin callers only see global
deployments plus their own team's BYOK rows; admins keep the full view.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-10 19:38:21 -07:00
Yassin Kortam
75509686f7 fix(auth_v2): reject SCIM id mutation and SCIM-shape auth errors
PATCH no longer lets a scim:write principal reassign the read-only id
attribute (RFC 7643): any operation whose path targets id, or a no-path value
object carrying id, is rejected with 400 instead of rewriting the record
identifier and clobbering another resource. Authentication failures on the
guarded SCIM routes now render the SCIM Error schema (RFC 7644) for 401 and
403 via a router-scoped route class, preserving the WWW-Authenticate
challenge, rather than the generic {detail} body.
2026-06-10 19:38:20 -07:00
Yassin Kortam
422c4df16e fix(auth_v2): close token-claim privilege escalation and related hardening
Token-flows and credential-flows security review fixes:

- H1 (privilege escalation): a validly-signed token could self-assert
  platform_admin and arbitrary teams via the roles/groups claims. Roles from a
  token are now filtered to a per-provider allowlist (OIDCProviderConfig.allowed_roles,
  default empty = none) with platform-level roles gated behind an explicit
  allow_platform_roles flag; groups become authoritative TeamIdentity only when they
  resolve to a provisioned SCIM Group in the store. Introspection responses carry no
  roles (no per-provider policy applies).
- M1: enforce iss on introspection (OAuth2IntrospectionConfig.issuer) in addition to aud.
- M2: bound JWKS refetch with cache_jwk_set + a 300s lifespan and add a 10s PyJWKClient
  timeout, so an unknown-kid stream can't amplify into per-request network fetches.
- M3: require https for issuer/jwks_uri/introspection_endpoint (loopback excepted for dev).
- rbac: anchor the Casbin act matcher (^(...)$) so a "GET" policy can't grant "GETX".
- basic auth: switch the reference store to pbkdf2_hmac-sha256 (600k iterations); the
  verifier protocol still lets deployments plug argon2/bcrypt.
- LOW: generic invalid_token description instead of echoing PyJWT internals; guard the
  introspection response.json() and require active to be boolean true.
2026-06-10 19:32:44 -07:00
Yassin Kortam
9b9cc60994 refactor(auth_v2): split oidc/saml/scim into sub-packages
Per the revised design §2, each protocol that carries its own config/routes
becomes a sub-package while the shared core stays flat. oidc.py -> oidc/router.py
with oidc/config.py (OIDCProviderConfig); saml.py -> saml/router.py with
saml/config.py (SAMLConfig + the attribute-map default); scim.py -> scim/router.py.
Each sub-package __init__ re-exports its public names so call sites read
from litellm.proxy.auth_v2.saml import SAMLConfig, build_saml_router. SessionConfig
moves next to the SessionStore it configures in session.py. git mv preserves
history; AuthConfig now composes the protocol configs from their sub-packages.
2026-06-10 19:25:44 -07:00
Yassin Kortam
0ee4397a59 test(auth_v2): adapt the suite to the AuthSecurity refactor and renames
Repoint the whole test surface off install_auth/app.state onto the AuthSecurity
composition root: build AuthSecurity(config, store, ...) and declare routes with
Security(auth.principal[, scopes]), auth.require_roles, auth.require_permission;
mount routers via build_*_router(auth). Apply the PEP8 renames (JWTVerifier,
APIKeyAuthenticator, OIDCAuthenticator, MutualTLSAuthenticator, OIDCProviderConfig,
SAMLConfig, MutualTLSConfig, RBACEngine.has_any_role). SAML moves to the shared
SessionStore + "litellm_session" cookie and session.safe_relay_state; the
build_authenticators tests assert concrete types now that the scheme attribute is
gone. No coverage lost; 151 tests pass.

Note: routes use the default-value Security() style instead of Annotated[...]
because this module runs under future annotations, where an Annotated marker is
stringified and FastAPI re-evaluates it in module globals, which cannot see the
closure-local auth instance.
2026-06-10 19:24:10 -07:00
Yassin Kortam
5f02c88369 fix(auth_v2): authenticate OIDC login sessions and adapt SCIM to AuthSecurity
The OIDC callback now mints a server-side session and sets the shared session
cookie (httponly, secure, samesite=lax) before redirecting, so login yields an
authenticated session that the shared SessionAuthenticator resolves; previously
it returned the user record as JSON and left the caller unauthenticated. The
login flow owns its CSRF protection without Starlette SessionMiddleware: it
stores state, nonce and the PKCE (S256) verifier in the short-lived
oauth_txn_store keyed by a temporary cookie, then on callback consumes the
transaction one-time, checks the returned state, exchanges the code with the
verifier and validates the nonce against the id_token. A replayed or expired
state finds no transaction and returns 400.

SCIM moves onto the AuthSecurity DI surface: build_scim_router(auth) reads
auth.resolver and guards writes with Security(auth.principal, scopes=
["scim:write"]) instead of app.state and get_current_principal.
2026-06-10 19:15:43 -07:00
Yassin Kortam
4ec48302a5 docs(auth_v2): one-line docstrings on AuthSecurity Security() entrypoints (D1) 2026-06-10 19:10:08 -07:00
Yassin Kortam
8302f55995 refactor(auth_v2): replace install_auth with AuthSecurity DI
Per user direction, drop install_auth/AuthContext/app.state entirely; the
enforcement layer is now an AuthSecurity object whose bound methods are the
FastAPI Security() dependencies. The app constructs AuthSecurity(config, resolver)
once and passes auth.principal / auth.require_roles / auth.require_permission to
Security(); routers take the instance explicitly via build_*_router(auth) and read
auth.resolver/auth.config rather than request.app.state.

Browser sessions are unified behind a shared SessionStore + SessionAuthenticator
(session.py): one cookie, keyed on identity["method"], so SAML and the upcoming
OIDC login flow share one store. AuthSecurity owns the post-login session_store
and a short-TTL oauth_txn_store for OIDC state/nonce/PKCE; SessionConfig moves the
cookie/TTL/redirect settings off SAMLConfig. SAML keeps its protocol-specific
outstanding/replay state local.

Fold in the standing judge findings: collapse the triplicated http/oauth2/oidc
bearer paths into one _authenticate_bearer_jwt helper, inject the introspection
async-client via a factory instead of importing litellm inline, drop the dead
scheme attribute from the authenticators, and rename to PEP 8 acronym casing
(JWTVerifier, OIDCAuthenticator, OIDCProviderConfig, SAMLConfig, RBACEngine,
APIKeyAuthenticator, MutualTLSAuthenticator). __all__ now exports AuthSecurity,
Role and the resolver protocols.

scim.py and oidc.py move to build_*_router(auth) separately.
2026-06-10 19:08:13 -07:00
Yassin Kortam
c512a49fa5 test(auth_v2): pin the hardened auth behaviors from the security fixes
Cover the security fixes landed in 71a189b, ca896ac, 6f3fc5e and 4503499:

- HTTP basic now verifies the password via an injected BasicAuthVerifier:
  correct creds 200, wrong password / unknown user / no verifier wired all 401
  (fail closed), and the password is never carried on the credential; plus a
  unit test that hash_basic_password is salted and InMemoryBasicAuthStore
  verifies it
- mTLS only trusts the forwarded subject-DN header from a peer inside the
  trusted-proxy CIDRs; a forged header from an untrusted peer is ignored
- SCIM discovery endpoints (ServiceProviderConfig, ResourceTypes, Schemas) are
  public, Users/Groups stay guarded, DELETE on a missing resource is a SCIM 404
  Error, and PATCH honors nested dotted paths while rejecting filter paths 400
- SAML ACS sets a Secure session cookie, binds the redirect target server-side
  so a client-supplied form RelayState is never trusted (falls back to the
  default path), and the session store enforces TTL expiry and size eviction

Mutation-checked: removing the basic-auth password check or the mTLS
trusted-peer gate fails these.
2026-06-10 18:59:41 -07:00
Yassin Kortam
450349965c fix(auth_v2): honor nested SCIM patch paths, align /Schemas, unshadow filter
PATCH now applies dotted attribute paths like name.givenName instead of
silently dropping them, and rejects unsupported value-filter paths
(emails[type eq "work"].value) with a 400 SCIM Error so behavior matches the
advertised patch support. /Schemas now uses the ListResponse envelope like the
other discovery endpoints, and the list route's query parameter no longer
shadows the builtin while keeping the RFC 7644 ?filter= wire contract.
2026-06-10 18:52:58 -07:00
Yassin Kortam
6f3fc5eba4 fix(auth_v2): close mTLS spoofing, introspection audience, SAML replay, and deactivated-user gaps
Address the SSO and credential-flow security review findings:

- mTLS (HIGH): the forwarded subject-DN header was trusted unconditionally, so
  any caller could send it and mint a service-account principal. Trust it only
  when the immediate peer is inside trusted_proxy_cidrs (same model as XFF) and
  fail closed otherwise; the ASGI-TLS-extension path already fails closed when
  no verified cert is present.
- OAuth2 introspection (HIGH): RFC 7662 responses were accepted regardless of
  audience. Enforce the response aud against OAuth2IntrospectionConfig.audience
  and reject active tokens whose audience does not match.
- SAML (HIGH): default allow_unsolicited to False so IdP-initiated/login-CSRF
  responses are rejected, add a single-use assertion-id replay cache, and bind
  the post-login redirect to the RelayState stored against the matched
  InResponseTo request rather than trusting the echoed form field.
- Deactivated users (M1): the resolver now rejects a credential that resolves to
  a SCIM user with active=False, so deactivation actually blocks authentication.
- Stop carrying underscore-prefixed carrier keys (raw api key, basic password)
  into Principal.claims, which is documented for audit logging.
2026-06-10 18:52:26 -07:00
ryan-crabbe-berri
4def6916da
refactor(ui): consolidate dashboard to one shell in the (dashboard) layout (#30166)
* refactor(ui): consolidate dashboard to one shell in the (dashboard) layout

Moves the legacy ?page= switch page into the (dashboard) route group and
hoists Navbar, sidebar, ThemeProvider, and DebugWarningBanner into the
shared layout with real props, deleting the degraded duplicate shell that
wrapped migrated routes. The active page key now derives from the URL at
render time, so navigating between legacy and migrated pages no longer
remounts the shell.

useProxySettings becomes a React Query hook taking accessToken, shared by
the navbar, the AdminPanel arm, and migrated pages; this replaces the
lifted proxySettings state and the Navbar setProxySettings prop drilling.
The invitation onboarding flow (?invitation_id=) keeps rendering without
chrome via a layout escape hatch. Dead dark mode state and the no-op antd
ConfigProvider are removed.

* fix(ui): include accessToken in useProxySettings query key

The queryFn closes over accessToken, so the key must include it for the
cache to be honest about its inputs. Settings are instance-global today,
which made the omission harmless, but a token change while mounted would
have served the cached entry without refetching.

* test(ui): point CreateKeyPage test at the moved page

The page moved into the (dashboard) route group and no longer renders
the navbar (the layout owns chrome now), so the valid-token test asserts
the default page content (UserDashboard stub) instead.
2026-06-10 18:37:44 -07:00
Yassin Kortam
ca896ac073 fix(auth_v2): make SCIM discovery public and return 404 on missing DELETE
RFC 7644 requires /ServiceProviderConfig, /ResourceTypes and /Schemas to be
publicly readable; split them onto an unguarded router while Users and Groups
stay behind scim:write. DELETE on a missing User or Group now returns a 404
SCIM Error instead of a misleading 204.
2026-06-10 18:34:37 -07:00
Yassin Kortam
71a189bf65 fix(auth_v2): harden HTTP basic, SAML sessions, and JWKS fetch
Address Greptile security findings in the authenticator, SAML and config layers:

- HTTP Basic accepted any password and copied the cleartext password into
  Principal.claims. Verify the password against an injected BasicAuthVerifier
  (InMemoryBasicAuthStore holds username -> salted sha256, constant-time compared
  with hmac.compare_digest) and stop putting the password in the credential;
  basic with no configured verifier now rejects rather than trusting the caller.
- SAML session cookie gains the Secure flag (httponly and samesite=lax already
  set), gated by SamlConfig.cookie_secure.
- SAML session store gains TTL expiry and max-size eviction
  (SamlConfig.session_ttl_seconds / session_max_size) so it can no longer grow
  unbounded or hand out stale sessions.
- JWKS signing-key lookup ran synchronously inside the async request path and
  blocked the event loop on a cache miss; run the JWT verify off-loop via
  starlette run_in_threadpool on the http-bearer, oauth2 at+jwt and oidc paths.
2026-06-10 18:34:10 -07:00
Yassin Kortam
2dd336a795 test(auth_v2): move tests under proxy/ to mirror the module relocation
The module moved from litellm/auth_v2 to litellm/proxy/auth_v2 (commit 104a5e1),
so the mirrored tests move from tests/test_litellm/auth_v2 to
tests/test_litellm/proxy/auth_v2 and their imports switch to
litellm.proxy.auth_v2. No behavior change; 134 tests still pass at the new path.
2026-06-10 18:32:12 -07:00
Yassin Kortam
104a5e1443 refactor(auth_v2): move module under litellm/proxy/auth_v2
The module imports FastAPI and is proxy-only, so it belongs under litellm/proxy
beside the legacy litellm/proxy/auth rather than at the top level. git mv
preserves history; the package is self-contained so the relative imports are
unchanged, and the scim2-models mypy override is path-independent.
2026-06-10 18:28:14 -07:00
Yassin Kortam
55a332bb91 test(auth_v2): cover Casbin-backed RBAC hierarchy and permissions
RBAC moved to an embedded Casbin enforcer: require_roles now honors the role
hierarchy and require_permission gates object/action against the policy.

- rbac: RbacEngine.has_role inherits down the g-rules (platform_admin satisfies
  an org_admin/team_member gate, org_admin satisfies org_viewer, team_admin
  satisfies team_member) and never climbs (team_member fails an org_admin gate);
  enforce honors the default policy (platform_admin any obj/act incl keyMatch2
  on /scim/v2/*, platform_viewer read-only, org_viewer no write) and an operator
  CSV fully replaces the in-code defaults
- security: require_roles passes a higher role through a lower-role gate via the
  hierarchy; require_permission allows platform_admin, denies a viewer on write
  with detail "Forbidden", and 401s when unauthenticated; an RbacEngine injected
  onto the AuthContext overrides the default policy (operator CSV path)

Replaces the removed has_any_role coverage. Mutation-checked: dropping the
hierarchy lookup or short-circuiting enforce fails these.
2026-06-10 18:26:19 -07:00
Yassin Kortam
3dc660a135 test(auth_v2): cover OAuth2 token introspection over the cached async client 2026-06-10 18:25:48 -07:00
Yassin Kortam
e309003c84 feat(auth_v2): back RBAC with Casbin
Replace the hand-rolled has_any_role set check with a Casbin-backed RbacEngine
(per design 03 §4). The engine wraps casbin.Enforcer over an embedded RBAC model
(request sub/obj/act, g role hierarchy, keyMatch2 on obj, regexMatch on act) and a
default in-code policy: platform_admin inherits org_admin/team_admin/platform_viewer,
org_admin inherits org_viewer, team_admin inherits team_member; grants platform_admin
/* .*, platform_admin /scim/v2/* .*, platform_viewer /* GET. Operators can replace the
whole policy with a CSV via AuthConfig.casbin_policy_path (FileAdapter); no DB adapter
yet.

require_roles now honors the hierarchy through the enforcer's grouping
(get_implicit_roles_for_user) instead of exact-match membership, so a platform_admin
passes a require_roles(ORG_ADMIN) gate; signature and 403 semantics are unchanged. New
require_permission(obj, act) dependency runs get_current_principal then RbacEngine.enforce
and 403s on deny. The engine is built in install_auth and injectable for tests via a new
rbac kwarg. Scope checks stay plain SecurityScopes (a token property, not policy).

Adds casbin to the proxy extra (pure python, no native deps).
2026-06-10 18:21:14 -07:00
Yassin Kortam
7cf35ccc0a test(auth_v2): cover SCIM scim:write guard and SAML RelayState redirect
Follow the auth module updates: SCIM routes now require the scim:write scope,
and the SAML ACS/login flow redirects to a validated RelayState instead of
returning JSON.

- scim: authenticate every request with a scoped key, and pin the guard
  directly: no credential -> 401 with WWW-Authenticate, an authenticated
  principal without scim:write -> 403 insufficient_scope
- saml: assert ACS returns 303 to a safe RelayState ("/dashboard") and falls
  back to default_redirect_path for an absolute/"//host" RelayState; assert
  GET /login threads ?next= through as a validated RelayState; add a garbage
  SAMLResponse -> 401 case

A mutation spot-check confirmed the open-redirect tests fail when the
_safe_relay_state guard is bypassed.
2026-06-10 18:06:06 -07:00
Yassin Kortam
d3878ee9a9 fix(auth_v2): require scim:write auth on all SCIM routes
The SCIM router mounted /scim/v2/* with no security dependency, so any caller
could create or delete users and groups unauthenticated. Guard the whole router
with Security(get_current_principal, scopes=["scim:write"]) per design 03 §11, so
provisioning callers authenticate with the same bearer token or API key as every
other route and the scope gates them: unauthenticated requests now 401, an
authenticated principal without scim:write gets 403 insufficient_scope.

Also document two deployment facts uncovered alongside this: uvicorn's
--proxy-headers rewrites request.client from X-Forwarded-For before this module's
trusted_proxy_cidrs check runs and silently bypasses it (install_auth docstring),
and the scheme_order precedence where HTTP precedes openIdConnect so a bearer JWT
is labeled bearer_jwt rather than oidc (both verify identically).
2026-06-10 18:03:08 -07:00
Yassin Kortam
03faccea17 test(auth_v2): add test suite for the standards-based auth module
Cover every layer of litellm/auth_v2 with tests that fail when the behavior
regresses, not just for coverage. Highlights:

- authenticators: JwtVerifier enforces signature, aud, iss, exp, required
  claims, and at+jwt typ via an injected jwks_client (real RS256 against an
  in-test RSA keypair, no monkeypatching); per-scheme apiKey/http-bearer/
  http-basic/oauth2/oidc/mTLS extraction and fail-fast on present-but-invalid
- security: OR precedence first-match-wins, a present-but-invalid api key does
  not fall through to a valid bearer, scope -> 403 insufficient_scope, role ->
  403, missing credential -> 401 with WWW-Authenticate, network wired onto the
  principal
- resolver: sha256 api-key lookup (wrong key never resolves), claims-driven
  principal build (groups -> teams, roles filtered to the Role enum), mTLS ->
  service account
- network: trusted-proxy XFF honored only from a trusted peer, right-to-left
  parse skips chained proxies, spoofed XFF from an untrusted peer ignored
- scim: Users/Groups create/get/patch/list/delete round-trip plus malformed
  body -> SCIM 400 Error and discovery endpoints
- oidc: userinfo -> scim2_models.User mapping and the upsert seam
- saml: a real pysaml2 IdP mints a signed assertion; ACS provisions the user,
  sets a session cookie, and authenticates with method=saml, while tampered and
  unsigned assertions are rejected (skipped when xmlsec1 is absent)
- models/rbac/config: frozen Credential, Role validation, scope/role helpers,
  SamlConfig metadata validation

A mutation spot-check confirmed the suite fails when JWT verification or the
api-key hash lookup is broken.
2026-06-10 17:59:29 -07:00
Yassin Kortam
da1a088a4f feat(auth_v2): redirect after SAML ACS with validated RelayState
Replace the test-convenience JSON body from /acs with the standard SP flow: set
the session cookie, then 303 redirect to the RelayState the IdP echoes back, or
to SamlConfig.default_redirect_path (default "/") when it is absent. RelayState
is validated to block open redirects - only relative paths are honored (must
start with a single "/", reject "//", any scheme, and backslashes), and
anything else falls back to the default. GET /login threads a ?next= query
param through as RelayState with the same validation so the post-login landing
page survives the round trip.
2026-06-10 17:40:05 -07:00
Yassin Kortam
677762bf60 refactor(auth_v2): align SamlConfig with the revised design doc
Match the updated 03-design.md SAML spec: rename sp_entity_id to entity_id,
collapse the split idp_metadata_path/idp_metadata_inline into one idp_metadata
field accepting inline XML, a local path, or a remote URL, and default the
attribute_map to the common Okta/Entra claims (email, givenName, surname,
groups). Make assertion signing mandatory by hardcoding want_assertions_signed
rather than exposing it as a togglable field. Map givenName/surname into the
SCIM User's Name (given/family/formatted) and email into emails, and fail the
ACS closed with 401 on any parse or signature-verification error.
2026-06-10 17:37:50 -07:00
Yassin Kortam
0b74ffa9c6 feat(auth_v2): implement full SAML 2.0 SP via pysaml2
Replace the deferred SAML thin-adapter stub with a working Service Provider
built on pysaml2: an SP metadata endpoint, an SP-initiated /login that
redirects to the IdP, and an ACS handling the HTTP-POST binding that verifies
the signed assertion, maps NameID and attribute statements into a
scim2_models.User, and upserts it through the same ProvisioningStore seam SCIM
and OIDC use. A SamlAuthenticator reads the post-ACS session cookie and resolves
to the one normalized Principal like every other scheme; AuthMethod gains a SAML
member. IdP metadata loads from a file path or inline XML via SamlConfig, and
install_auth mounts the router and authenticator when SAML is enabled.

pysaml2 pulls pyOpenSSL transitively without pinning it, and older pyOpenSSL
caps cryptography below 46 and breaks at import against the version this proxy
already requires; pin pyOpenSSL>=26 so the resolver stays on a
cryptography-46-compatible release. pysaml2 also needs the system xmlsec1
binary at runtime (brew install libxmlsec1 on macOS, apt-get install xmlsec1
libxmlsec1-dev on Debian); SamlConfig.xmlsec_binary can point at it when it is
not on PATH.
2026-06-10 17:33:02 -07:00
Yassin Kortam
a0a59a2197 feat(auth_v2): add standards-based auth and identity module
New additive litellm/auth_v2 package: a thin orchestration layer over PyJWT,
Authlib and scim2-models behind FastAPI's native Security() primitives that
normalizes every credential into one standards-shaped Principal carrying
org/team/user and network identity.

Authentication, identity resolution, authorization and enforcement are kept
as separate layers. Five authenticators cover the OpenAPI scheme types
(apiKey, http bearer-JWT/basic, oauth2 at+jwt + introspection, openIdConnect,
mutualTLS); a shared JwtVerifier enforces signature, issuer, audience and exp
on every JWT path via a cached PyJWKClient. RBAC is a flat Role enum plus
scope/role checks wired through SecurityScopes. Missing or invalid credentials
return 401 with an RFC 9110/6750 WWW-Authenticate challenge, scope failures
return 403 insufficient_scope. SCIM 2.0 Users/Groups/PATCH/discovery and an
Authlib OIDC login flow share one ProvisioningStore seam; the SAML SP is a
documented thin adapter pending pysaml2.

The module is unimported by the proxy app and depends on nothing in
litellm/proxy/auth.
2026-06-10 17:22:20 -07:00
Yassin Kortam
2bbf688613 build(auth_v2): add Authlib and scim2-models for the auth_v2 module
Pull in the OSS libraries the standards-based auth module orchestrates:
Authlib for the OIDC login flow and scim2-models for SCIM 2.0, and switch
PyJWT to the [crypto] extra so JWKS-backed RS256 verification is explicit
(cryptography was already a proxy dependency). scim2-models ships py.typed
but its generic, alias-driven models trip mypy's call-arg check though they
work at runtime, so treat the library as untyped at the boundary in both
litellm/mypy.ini (used by CI) and the root pyproject mypy config.
2026-06-10 17:22:20 -07:00
ryan-crabbe-berri
496f5b9859
fix(ui): dev server 404s on migrated-page links because uiBase hardcodes /ui (#30169)
* fix(ui): serve migrated-page links unprefixed on the dev server

migratedHref and legacyPageHref always prepended /ui, which is where the
proxy mounts the static export but not where next dev serves the app
(basePath is empty; the app lives at the root on localhost:3000). Every
sidebar link to a migrated page and every ?page= bookmark redirect
therefore 404'd in dev, and would do so for each page cut over in the
App Router migration.

uiBase now returns the bare root under NODE_ENV=development. The check
is inlined at build time, so production output is unchanged for both
the default /ui mount and server_root_path deployments.

* test(ui): pin NODE_ENV in production-mode migratedPages tests

The production-mode describes relied on vitest defaulting NODE_ENV to
test; a developer with NODE_ENV=development exported in their shell
would see them fail. Stub it explicitly so the suite is deterministic
regardless of ambient environment.
2026-06-11 00:16:36 +00:00
Yassin Kortam
da9d64b4de
fix(proxy): return 5xx on DB infra errors during auth; reserve 401 for genuine auth failures (#29986) 2026-06-10 23:48:11 +00:00
Mateo Wang
ba72ccf52c
feat: add conventional commits and coding guidelines (#30159)
* feat: add guideline for conventional commits

* feat: add functional programming coding conventions
2026-06-10 16:34:08 -07:00
yuneng-jiang
b301d306c2
fix(release): stop backport releases from overwriting the latest badge (#30005)
create-release published every release with GitHub's default make_latest,
which is true, so any newly published stable release claimed the repo
"Latest" badge regardless of version. That let a backport like 1.84.6
overwrite a newer line like 1.88.1 as latest.

Compute make_latest explicitly: a stable release only claims latest when
its version is >= the current latest (via getLatestRelease), backports to
an older line publish with make_latest false, and prereleases never claim
latest. Version comparison accounts for the maintenance suffix (.postN and
legacy -stable.patch.N) so within-line ordering stays correct
2026-06-10 16:33:48 -07:00
Yassin Kortam
dff25fef44
feat(proxy): add option to disable server-side prepared statements for DB lookups (#29984) 2026-06-10 16:06:32 -07:00