Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/types.py: 36%
30 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"""
2Types for the management endpoints
4Might include fastapi/proxy requirements.txt related imports
5"""
7from collections.abc import Iterable, Sequence
8from typing import Any, Final, cast
10from fastapi_sso.sso.base import OpenID
12from litellm.proxy._types import LitellmUserRoles
14# Ordered highest to lowest privilege
15LITELLM_USER_ROLE_HIERARCHY: Final = (
16 LitellmUserRoles.PROXY_ADMIN,
17 LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
18 LitellmUserRoles.INTERNAL_USER,
19 LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
20)
23def highest_privilege_role(roles: Iterable[LitellmUserRoles]) -> LitellmUserRoles | None:
24 """
25 Pick the highest privilege role out of the roles an IdP asserted for one user.
27 IdPs do not guarantee ordering within a multi-valued role claim, so a user holding
28 several roles resolves to the most privileged one rather than whichever came first.
29 Roles the hierarchy does not rank (org_admin, team, customer) resolve by name to stay
30 deterministic.
32 Args:
33 roles: The roles resolved from the claim
35 Returns:
36 The highest privilege role, or None if `roles` is empty
37 """
38 resolved: Final = frozenset(roles)
39 if not resolved:
40 return None
42 ranked: Final = next((role for role in LITELLM_USER_ROLE_HIERARCHY if role in resolved), None)
43 return ranked if ranked is not None else min(resolved, key=lambda role: role.value)
46def is_valid_litellm_user_role(role_str: str) -> bool:
47 """
48 Check if a string is a valid LitellmUserRoles enum value (case-insensitive).
50 Args:
51 role_str: String to validate (e.g., "proxy_admin", "PROXY_ADMIN", "internal_user")
53 Returns:
54 True if the string matches a valid LitellmUserRoles value, False otherwise
55 """
56 try:
57 # Use _value2member_map_ for O(1) lookup, case-insensitive
58 return role_str.lower() in LitellmUserRoles._value2member_map_
59 except Exception:
60 return False
63def _role_from_claim_value(role_str: object) -> LitellmUserRoles | None:
64 if not isinstance(role_str, str):
65 return None
66 # Use _value2member_map_ for O(1) lookup, case-insensitive
67 result: Final = LitellmUserRoles._value2member_map_.get(role_str.lower())
68 return cast(LitellmUserRoles | None, result)
71def get_litellm_user_role(role_str: object) -> LitellmUserRoles | None:
72 """
73 Convert a string (or list of strings) to a LitellmUserRoles enum if valid (case-insensitive).
75 Handles list inputs since some SSO providers (e.g., Keycloak) return roles
76 as arrays like ["proxy_admin"] instead of plain strings. A claim carrying several
77 roles resolves to the highest privilege one, so a user does not lose access just
78 because the IdP listed a weaker role first.
80 Args:
81 role_str: String or list to convert (e.g., "proxy_admin", ["proxy_admin"])
83 Returns:
84 LitellmUserRoles enum if valid, None otherwise
85 """
86 if isinstance(role_str, (list, tuple)):
87 entries: Final = cast(Sequence[object], role_str) # cast-ok: isinstance narrows the claim, not its elements
88 return highest_privilege_role(
89 role for role in (_role_from_claim_value(entry) for entry in entries) if role is not None
90 )
91 return _role_from_claim_value(role_str)
94class CustomOpenID(OpenID):
95 team_ids: list[str]
96 user_role: LitellmUserRoles | None = None
97 extra_fields: dict[str, Any] | None = None