mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui_sso): select most permissive role when SSO payload has multiple app_roles
This commit is contained in:
parent
9c59e2ae55
commit
f08ded5324
3 changed files with 70 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue