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

1""" 

2Types for the management endpoints 

3 

4Might include fastapi/proxy requirements.txt related imports 

5""" 

6 

7from collections.abc import Iterable, Sequence 

8from typing import Any, Final, cast 

9 

10from fastapi_sso.sso.base import OpenID 

11 

12from litellm.proxy._types import LitellmUserRoles 

13 

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) 

21 

22 

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. 

26 

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. 

31 

32 Args: 

33 roles: The roles resolved from the claim 

34 

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 

41 

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) 

44 

45 

46def is_valid_litellm_user_role(role_str: str) -> bool: 

47 """ 

48 Check if a string is a valid LitellmUserRoles enum value (case-insensitive). 

49 

50 Args: 

51 role_str: String to validate (e.g., "proxy_admin", "PROXY_ADMIN", "internal_user") 

52 

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 

61 

62 

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) 

69 

70 

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

74 

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. 

79 

80 Args: 

81 role_str: String or list to convert (e.g., "proxy_admin", ["proxy_admin"]) 

82 

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) 

92 

93 

94class CustomOpenID(OpenID): 

95 team_ids: list[str] 

96 user_role: LitellmUserRoles | None = None 

97 extra_fields: dict[str, Any] | None = None