From 341e75ab459178f5ec0b3e74815c3d87875bd763 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Wed, 10 Jun 2026 19:50:23 -0700 Subject: [PATCH] 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. --- tests/test_litellm/proxy/auth_v2/conftest.py | 2 +- .../proxy/auth_v2/test_authenticators.py | 2 +- .../test_litellm/proxy/auth_v2/test_config.py | 3 +- tests/test_litellm/proxy/auth_v2/test_oidc.py | 4 +-- .../proxy/auth_v2/test_resolver.py | 34 +++++++++++++++---- tests/test_litellm/proxy/auth_v2/test_saml.py | 10 +++--- .../proxy/auth_v2/test_security.py | 2 +- 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/tests/test_litellm/proxy/auth_v2/conftest.py b/tests/test_litellm/proxy/auth_v2/conftest.py index 2131ab25c78..32d89d78cfe 100644 --- a/tests/test_litellm/proxy/auth_v2/conftest.py +++ b/tests/test_litellm/proxy/auth_v2/conftest.py @@ -7,7 +7,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from litellm.proxy.auth_v2.authenticators import JWTVerifier -from litellm.proxy.auth_v2.config import OIDCProviderConfig +from litellm.proxy.auth_v2 import OIDCProviderConfig from auth_v2_helpers import TEST_AUDIENCE, TEST_ISSUER, FakeJwksClient, TokenFactory diff --git a/tests/test_litellm/proxy/auth_v2/test_authenticators.py b/tests/test_litellm/proxy/auth_v2/test_authenticators.py index b3da31202ce..20d1bb0ef9a 100644 --- a/tests/test_litellm/proxy/auth_v2/test_authenticators.py +++ b/tests/test_litellm/proxy/auth_v2/test_authenticators.py @@ -23,9 +23,9 @@ from litellm.proxy.auth_v2.config import ( HttpBasicConfig, MutualTLSConfig, OAuth2IntrospectionConfig, - OIDCProviderConfig, TrustedProxyConfig, ) +from litellm.proxy.auth_v2 import OIDCProviderConfig from litellm.proxy.auth_v2.errors import AuthError from litellm.proxy.auth_v2.models import AuthMethod diff --git a/tests/test_litellm/proxy/auth_v2/test_config.py b/tests/test_litellm/proxy/auth_v2/test_config.py index fd4c26100be..7958660b821 100644 --- a/tests/test_litellm/proxy/auth_v2/test_config.py +++ b/tests/test_litellm/proxy/auth_v2/test_config.py @@ -5,9 +5,8 @@ from pydantic import ValidationError from litellm.proxy.auth_v2.config import ( OAuth2IntrospectionConfig, - OIDCProviderConfig, - SAMLConfig, ) +from litellm.proxy.auth_v2 import OIDCProviderConfig, SAMLConfig def test_saml_config_requires_idp_metadata_when_enabled(): diff --git a/tests/test_litellm/proxy/auth_v2/test_oidc.py b/tests/test_litellm/proxy/auth_v2/test_oidc.py index 6c816dd338f..36c5bca1bc2 100644 --- a/tests/test_litellm/proxy/auth_v2/test_oidc.py +++ b/tests/test_litellm/proxy/auth_v2/test_oidc.py @@ -1,7 +1,7 @@ from __future__ import annotations -from litellm.proxy.auth_v2.config import OIDCProviderConfig -from litellm.proxy.auth_v2.oidc import _provider_key, _user_from_userinfo +from litellm.proxy.auth_v2 import OIDCProviderConfig +from litellm.proxy.auth_v2.oidc.router import _provider_key, _user_from_userinfo from litellm.proxy.auth_v2.resolver import InMemoryIdentityStore diff --git a/tests/test_litellm/proxy/auth_v2/test_resolver.py b/tests/test_litellm/proxy/auth_v2/test_resolver.py index 3c534b6a4d6..89128c71f28 100644 --- a/tests/test_litellm/proxy/auth_v2/test_resolver.py +++ b/tests/test_litellm/proxy/auth_v2/test_resolver.py @@ -1,6 +1,7 @@ from __future__ import annotations import pytest +from scim2_models import Group as ScimGroup from litellm.proxy.auth_v2.errors import AuthError from litellm.proxy.auth_v2.models import ( @@ -80,9 +81,8 @@ async def test_subject_lookup_prefers_stored_principal(): assert resolved.subject == "from-store" -async def test_self_describing_token_builds_principal_from_claims(): - store = InMemoryIdentityStore() - credential = Credential( +def _oidc_credential(**claims) -> Credential: + return Credential( scheme=SecuritySchemeType.OPENID_CONNECT, method=AuthMethod.OIDC, subject="sub-42", @@ -92,19 +92,39 @@ async def test_self_describing_token_builds_principal_from_claims(): "email": "dana@example.com", "preferred_username": "dana", "name": "Dana D", - "groups": ["eng", "oncall"], - "roles": ["org_admin", "bogus_role"], + **claims, }, ) - principal = await store.resolve(credential) + + +async def test_self_describing_token_builds_principal_from_claims(): + store = InMemoryIdentityStore() + principal = await store.resolve(_oidc_credential(roles=["org_admin", "bogus_role"])) assert principal.user.email == "dana@example.com" assert principal.user.user_name == "dana" - assert [team.id for team in principal.teams] == ["eng", "oncall"] # invalid role strings are filtered out, valid ones become Role enums assert principal.roles == [Role.ORG_ADMIN] assert principal.scopes == ["models:read"] +async def test_group_claim_without_provisioned_scim_group_is_not_a_team(): + # H1: a token group claim is not authoritative on its own + store = InMemoryIdentityStore() + principal = await store.resolve(_oidc_credential(groups=["eng", "oncall"])) + assert principal.teams == [] + + +async def test_group_claim_becomes_team_only_when_provisioned(): + store = InMemoryIdentityStore( + groups={"eng": ScimGroup(id="eng", display_name="Engineering")} + ) + principal = await store.resolve(_oidc_credential(groups=["eng", "unprovisioned"])) + # only the provisioned group resolves to a team; the unknown one is dropped + assert len(principal.teams) == 1 + assert principal.teams[0].id == "eng" + assert principal.teams[0].name == "Engineering" + + async def test_mtls_credential_resolves_to_service_account(): store = InMemoryIdentityStore() credential = Credential( diff --git a/tests/test_litellm/proxy/auth_v2/test_saml.py b/tests/test_litellm/proxy/auth_v2/test_saml.py index f5ade9b0863..c674dc48c29 100644 --- a/tests/test_litellm/proxy/auth_v2/test_saml.py +++ b/tests/test_litellm/proxy/auth_v2/test_saml.py @@ -94,7 +94,7 @@ def saml_env(tmp_path: Path) -> SamlEnv: from saml2.saml import NAMEID_FORMAT_EMAILADDRESS from saml2.server import Server - from litellm.proxy.auth_v2.config import SAMLConfig + from litellm.proxy.auth_v2 import SAMLConfig idp_key, idp_cert = _gen_cert(tmp_path, "idp") sp_key, sp_cert = _gen_cert(tmp_path, "sp") @@ -355,8 +355,8 @@ def test_login_rejects_open_redirect_next(saml_env): def test_map_attributes_applies_attribute_map(): - from litellm.proxy.auth_v2.config import DEFAULT_SAML_ATTRIBUTE_MAP - from litellm.proxy.auth_v2.saml import _map_attributes + from litellm.proxy.auth_v2.saml.config import DEFAULT_SAML_ATTRIBUTE_MAP + from litellm.proxy.auth_v2.saml.router import _map_attributes ava = { "email": ["alice@example.com"], @@ -372,7 +372,7 @@ def test_map_attributes_applies_attribute_map(): def test_user_from_mapped_builds_name_and_email(): - from litellm.proxy.auth_v2.saml import _user_from_mapped + from litellm.proxy.auth_v2.saml.router import _user_from_mapped user = _user_from_mapped( "alice@example.com", @@ -413,7 +413,7 @@ def test_safe_relay_state_blocks_open_redirects(candidate, expected): ], ) def test_metadata_source_classifies_input(metadata, expected_key): - from litellm.proxy.auth_v2.saml import _metadata_source + from litellm.proxy.auth_v2.saml.router import _metadata_source assert expected_key in _metadata_source(metadata) diff --git a/tests/test_litellm/proxy/auth_v2/test_security.py b/tests/test_litellm/proxy/auth_v2/test_security.py index 96d240d86e5..e2f64fb44fc 100644 --- a/tests/test_litellm/proxy/auth_v2/test_security.py +++ b/tests/test_litellm/proxy/auth_v2/test_security.py @@ -15,8 +15,8 @@ from litellm.proxy.auth_v2.config import ( ApiKeySchemeConfig, AuthConfig, HttpBasicConfig, - OIDCProviderConfig, ) +from litellm.proxy.auth_v2 import OIDCProviderConfig from litellm.proxy.auth_v2.models import AuthMethod, Principal, PrincipalType from litellm.proxy.auth_v2.rbac import RBACEngine, Role from litellm.proxy.auth_v2.resolver import InMemoryIdentityStore, _hash_api_key