fix(ui_sso): select most permissive role when SSO payload has multiple app_roles

This commit is contained in:
Devin AI 2026-07-15 20:12:49 +00:00
parent 9c59e2ae55
commit f08ded5324
3 changed files with 70 additions and 8 deletions

View file

@ -53,6 +53,40 @@ def get_litellm_user_role(role_str) -> Optional[LitellmUserRoles]:
return None
LITELLM_USER_ROLE_HIERARCHY: tuple[LitellmUserRoles, ...] = (
LitellmUserRoles.PROXY_ADMIN,
LitellmUserRoles.ORG_ADMIN,
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
LitellmUserRoles.INTERNAL_USER,
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
LitellmUserRoles.TEAM,
LitellmUserRoles.CUSTOMER,
)
def get_most_permissive_litellm_user_role(
role_strs: list[str],
) -> Optional[LitellmUserRoles]:
"""
Given a list of role strings from an SSO payload, return the most permissive
valid LitellmUserRoles according to LITELLM_USER_ROLE_HIERARCHY.
Some identity providers (e.g. Microsoft Entra with access packages) assign a
user multiple app_roles at once. Selecting the first array entry is arbitrary,
so pick the highest-privilege role instead.
Args:
role_strs: Role strings from the SSO payload (e.g. ["internal_user", "proxy_admin"])
Returns:
The most permissive valid LitellmUserRoles, or None if none are valid
"""
valid_roles = frozenset(
role for role in (get_litellm_user_role(role_str) for role_str in role_strs) if role is not None
)
return next((role for role in LITELLM_USER_ROLE_HIERARCHY if role in valid_roles), None)
class CustomOpenID(OpenID):
team_ids: List[str]
user_role: Optional[LitellmUserRoles] = None

View file

@ -102,6 +102,7 @@ from litellm.proxy.management_endpoints.team_endpoints import new_team, team_mem
from litellm.proxy.management_endpoints.types import (
CustomOpenID,
get_litellm_user_role,
get_most_permissive_litellm_user_role,
is_valid_litellm_user_role,
)
from litellm.proxy.utils import (
@ -3848,13 +3849,11 @@ class MicrosoftSSOHandler:
# Combine groups and app roles
user_role: Optional[LitellmUserRoles] = None
if app_roles:
# Check if any app role is a valid LitellmUserRoles
for role_str in app_roles:
role = get_litellm_user_role(role_str)
if role is not None:
user_role = role
verbose_proxy_logger.debug(f"Found valid LitellmUserRoles '{role.value}' in app_roles")
break
user_role = get_most_permissive_litellm_user_role(app_roles)
if user_role is not None:
verbose_proxy_logger.debug(
f"Selected most permissive LitellmUserRoles '{user_role.value}' from app_roles: {app_roles}"
)
verbose_proxy_logger.debug(f"Combined team_ids (groups + app roles): {user_team_ids}")

View file

@ -1,7 +1,11 @@
import jwt
import pytest
from litellm.proxy.management_endpoints.ui_sso import MicrosoftSSOHandler
from litellm.proxy.management_endpoints.types import get_litellm_user_role
from litellm.proxy.management_endpoints.types import (
get_litellm_user_role,
get_most_permissive_litellm_user_role,
)
from litellm.proxy._types import LitellmUserRoles
@ -89,3 +93,28 @@ def test_defaults_to_internal_user_viewer_when_no_role():
# Default role would be internal_user_viewer
default_role = LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
assert default_role.value == "internal_user_viewer"
@pytest.mark.parametrize(
"app_roles, expected",
[
# Regression for #33434: the most permissive role wins regardless of
# its position in the array, instead of always picking the first entry.
(["internal_user", "proxy_admin"], LitellmUserRoles.PROXY_ADMIN),
(["proxy_admin", "internal_user"], LitellmUserRoles.PROXY_ADMIN),
(
["internal_user_viewer", "internal_user", "proxy_admin_viewer"],
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
),
(["org_admin", "proxy_admin_viewer"], LitellmUserRoles.ORG_ADMIN),
# Unknown roles are ignored; the single valid one is selected.
(["some_custom_role", "internal_user"], LitellmUserRoles.INTERNAL_USER),
(["PROXY_ADMIN", "internal_user"], LitellmUserRoles.PROXY_ADMIN),
(["internal_user"], LitellmUserRoles.INTERNAL_USER),
# No valid roles at all.
(["some_custom_role", "another_unknown_role"], None),
([], None),
],
)
def test_selects_most_permissive_app_role(app_roles, expected):
assert get_most_permissive_litellm_user_role(app_roles) == expected