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.
This commit is contained in:
Yassin Kortam 2026-06-10 19:50:23 -07:00
parent a9be3d23e0
commit 341e75ab45
7 changed files with 38 additions and 19 deletions

View file

@ -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

View file

@ -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

View file

@ -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():

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -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