Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/config_resolvers/sso.py: 100%
27 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Resolved SSO config object.
3Reconciles the dedicated ``sso_config`` DB row (lowercase, per-value encrypted
4keys) with the process environment (uppercase env vars) into a typed
5``SSOConfig`` plus per-field provenance. This is the single source of truth for
6the SSO field -> env-var mapping, used by both the read-back endpoint and the
7save endpoint so the two can never drift.
8"""
10from collections.abc import Mapping
11from dataclasses import dataclass
12from typing import Final
14from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
15from litellm.proxy.config_resolvers._descriptors import (
16 FieldDescriptor,
17 FieldSource,
18 resolve_fields,
19)
20from litellm.types.proxy.management_endpoints.ui_sso import (
21 RoleMappings,
22 SSOConfig,
23 TeamMappings,
24)
26SSO_DESCRIPTORS: Final[tuple[FieldDescriptor, ...]] = (
27 FieldDescriptor("google_client_id", "google_client_id", "GOOGLE_CLIENT_ID"),
28 FieldDescriptor("google_client_secret", "google_client_secret", "GOOGLE_CLIENT_SECRET", is_secret=True),
29 FieldDescriptor("microsoft_client_id", "microsoft_client_id", "MICROSOFT_CLIENT_ID"),
30 FieldDescriptor("microsoft_client_secret", "microsoft_client_secret", "MICROSOFT_CLIENT_SECRET", is_secret=True),
31 FieldDescriptor("microsoft_tenant", "microsoft_tenant", "MICROSOFT_TENANT"),
32 FieldDescriptor("generic_client_id", "generic_client_id", "GENERIC_CLIENT_ID"),
33 FieldDescriptor("generic_client_secret", "generic_client_secret", "GENERIC_CLIENT_SECRET", is_secret=True),
34 FieldDescriptor(
35 "generic_authorization_endpoint", "generic_authorization_endpoint", "GENERIC_AUTHORIZATION_ENDPOINT"
36 ),
37 FieldDescriptor("generic_token_endpoint", "generic_token_endpoint", "GENERIC_TOKEN_ENDPOINT"),
38 FieldDescriptor("generic_userinfo_endpoint", "generic_userinfo_endpoint", "GENERIC_USERINFO_ENDPOINT"),
39 FieldDescriptor("generic_scope", "generic_scope", "GENERIC_SCOPE", default="openid email profile"),
40 FieldDescriptor("saml_idp_metadata_url", "saml_idp_metadata_url", "SAML_IDP_METADATA_URL"),
41 FieldDescriptor("saml_idp_metadata_xml", "saml_idp_metadata_xml", "SAML_IDP_METADATA_XML"),
42 FieldDescriptor("saml_sp_entity_id", "saml_sp_entity_id", "SAML_SP_ENTITY_ID"),
43 FieldDescriptor("saml_allow_unsolicited", "saml_allow_unsolicited", "SAML_ALLOW_UNSOLICITED"),
44 FieldDescriptor("proxy_base_url", "proxy_base_url", "PROXY_BASE_URL"),
45)
47# Derived from the descriptor table so read (masking) and the field->env mapping
48# never diverge from the resolver.
49SSO_SECRET_FIELDS: Final[frozenset[str]] = frozenset(d.field_name for d in SSO_DESCRIPTORS if d.is_secret)
50SSO_FIELD_ENV_VARS: Final[dict[str, str]] = {d.field_name: d.env_var for d in SSO_DESCRIPTORS}
52# Structured sub-objects stored on the SSO row that are not simple env-backed
53# scalars; handled outside the descriptor resolution.
54_STRUCTURED_KEYS: Final = ("role_mappings", "team_mappings")
57@dataclass(frozen=True, slots=True)
58class ResolvedSSOConfig:
59 config: SSOConfig
60 provenance: dict[str, FieldSource]
63def _decrypt(raw: Mapping[str, object]) -> dict[str, object]:
64 return {
65 key: (
66 decrypt_value_helper(value=value, key=key, return_original_value=True) if isinstance(value, str) else value
67 )
68 for key, value in raw.items()
69 }
72def _parse_role_mappings(data: object) -> RoleMappings | None:
73 # The stored row is JSON, so mappings arrive as a dict (or are absent).
74 return RoleMappings(**data) if isinstance(data, dict) else None
77def _parse_team_mappings(data: object) -> TeamMappings | None:
78 return TeamMappings(**data) if isinstance(data, dict) else None
81def resolve_sso_config(sso_db_settings: Mapping[str, object] | None, env: Mapping[str, str]) -> ResolvedSSOConfig:
82 """Resolve the effective SSO config: stored row first, then process env.
84 Decryption happens here, once, via the pure ``decrypt_value_helper``; this
85 function never writes ``os.environ`` (unlike the legacy read path). Values
86 are returned unmasked so the login path could consume them; the read-back
87 endpoint is responsible for masking secrets before responding to the UI.
88 """
89 raw: Final = dict(sso_db_settings) if sso_db_settings else {}
90 decrypted: Final = _decrypt({key: value for key, value in raw.items() if key not in _STRUCTURED_KEYS})
91 values, provenance = resolve_fields(SSO_DESCRIPTORS, decrypted, env)
92 structured: Final = {
93 "user_email": decrypted.get("user_email"),
94 "ui_access_mode": decrypted.get("ui_access_mode"),
95 "role_mappings": _parse_role_mappings(raw.get("role_mappings")),
96 "team_mappings": _parse_team_mappings(raw.get("team_mappings")),
97 }
98 config: Final = SSOConfig(**{**values, **structured})
99 return ResolvedSSOConfig(config=config, provenance=provenance)