6262 has_admin_ui_access ,
6363)
6464from litellm .proxy .management_endpoints .team_endpoints import new_team , team_member_add
65- from litellm .proxy .management_endpoints .types import CustomOpenID , get_litellm_user_role
65+ from litellm .proxy .management_endpoints .types import (
66+ CustomOpenID ,
67+ get_litellm_user_role ,
68+ is_valid_litellm_user_role ,
69+ )
6670from litellm .proxy .utils import (
6771 PrismaClient ,
6872 ProxyLogging ,
@@ -554,6 +558,20 @@ async def get_user_info_from_db(
554558
555559 return None
556560
561+ def _should_use_role_from_sso_response (sso_role : Optional [str ]) -> bool :
562+ """returns true if SSO upsert should use the 'role' defined on the SSO response"""
563+ if sso_role is None :
564+ return False
565+
566+ if not is_valid_litellm_user_role (sso_role ):
567+ verbose_proxy_logger .debug (
568+ f"SSO role '{ sso_role } ' is not a valid LiteLLM user role. "
569+ "Ignoring role from SSO response. See LitellmUserRoles enum for valid roles."
570+ )
571+ return False
572+ return True
573+
574+
557575
558576def apply_user_info_values_to_sso_user_defined_values (
559577 user_info : Optional [Union [LiteLLM_UserTable , NewUserResponse ]],
@@ -564,12 +582,22 @@ def apply_user_info_values_to_sso_user_defined_values(
564582 if user_info is not None and user_info .user_id is not None :
565583 user_defined_values ["user_id" ] = user_info .user_id
566584
567- # Check if user_role already exists in user_defined_values (from JWT/SSO response)
568- if user_defined_values .get ("user_role" ) is None :
585+ # SSO role takes precedence - only use DB role if SSO didn't provide one
586+ # This ensures SSO is the authoritative source for user roles
587+ sso_role = user_defined_values .get ("user_role" )
588+ db_role = user_info .user_role if user_info else None
589+
590+ if _should_use_role_from_sso_response (sso_role ):
591+ # SSO provided a valid role, keep it and log that we're using it
592+ verbose_proxy_logger .info (f"Using SSO role: { sso_role } (DB role was: { db_role } )" )
593+ else :
594+ # SSO didn't provide a valid role, fall back to DB role or default
569595 if user_info is None or user_info .user_role is None :
570- user_defined_values ["user_role" ] = LitellmUserRoles .INTERNAL_USER_VIEW_ONLY
596+ user_defined_values ["user_role" ] = LitellmUserRoles .INTERNAL_USER_VIEW_ONLY .value
597+ verbose_proxy_logger .debug ("No SSO or DB role found, using default: INTERNAL_USER_VIEW_ONLY" )
571598 else :
572599 user_defined_values ["user_role" ] = user_info .user_role
600+ verbose_proxy_logger .debug (f"Using DB role: { user_info .user_role } " )
573601
574602 # Preserve the user's existing models from the database
575603 if user_info is not None and hasattr (user_info , "models" ) and user_info .models :
@@ -1540,7 +1568,15 @@ def _get_user_email_and_id_from_result(
15401568 },
15411569 )
15421570
1543- # generic client id
1571+ # Extract user_role from result (works for all SSO providers)
1572+ if result is not None :
1573+ _user_role = getattr (result , "user_role" , None )
1574+ if _user_role is not None :
1575+ # Convert enum to string if needed
1576+ user_role = _user_role .value if isinstance (_user_role , LitellmUserRoles ) else _user_role
1577+ verbose_proxy_logger .debug (f"Extracted user_role from SSO result: { user_role } " )
1578+
1579+ # generic client id - override with custom attribute name if specified
15441580 if generic_client_id is not None and result is not None :
15451581 generic_user_role_attribute_name = os .getenv (
15461582 "GENERIC_USER_ROLE_ATTRIBUTE" , "role"
0 commit comments