mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
[Feat] UI SSO - allow fetching role from generic SSO provider (Keycloak) (#17787)
* fix ui SSO * TestGenericResponseConvertorUserRole * Assigning User Roles via SSO
This commit is contained in:
parent
439bb5bfe3
commit
5d456bcdc3
3 changed files with 91 additions and 1 deletions
|
|
@ -130,6 +130,17 @@ GENERIC_INCLUDE_CLIENT_ID = "false" # some providers enforce that the client_id
|
|||
GENERIC_SCOPE = "openid profile email" # default scope openid is sometimes not enough to retrieve basic user info like first_name and last_name located in profile scope
|
||||
```
|
||||
|
||||
**Assigning User Roles via SSO**
|
||||
|
||||
Use `GENERIC_USER_ROLE_ATTRIBUTE` to specify which attribute in the SSO token contains the user's role. The role value must be one of the following supported LiteLLM roles:
|
||||
|
||||
- `proxy_admin` - Admin over the platform
|
||||
- `proxy_admin_viewer` - Can login, view all keys, view all spend (read-only)
|
||||
- `internal_user` - Can login, view/create/delete their own keys, view their spend
|
||||
- `internal_user_view_only` - Can login, view their own keys, view their own spend
|
||||
|
||||
Nested attribute paths are supported (e.g., `claims.role` or `attributes.litellm_role`).
|
||||
|
||||
- Set Redirect URI, if your provider requires it
|
||||
- Set a redirect url = `<your proxy base url>/sso/callback`
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -265,6 +265,10 @@ def generic_response_convertor(
|
|||
"GENERIC_USER_PROVIDER_ATTRIBUTE", "provider"
|
||||
)
|
||||
|
||||
generic_user_role_attribute_name = os.getenv(
|
||||
"GENERIC_USER_ROLE_ATTRIBUTE", "role"
|
||||
)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f" generic_user_id_attribute_name: {generic_user_id_attribute_name}\n generic_user_email_attribute_name: {generic_user_email_attribute_name}"
|
||||
)
|
||||
|
|
@ -277,6 +281,17 @@ def generic_response_convertor(
|
|||
team_ids = jwt_handler.get_team_ids_from_jwt(cast(dict, response))
|
||||
all_teams.extend(team_ids)
|
||||
|
||||
# Extract user role from SSO response
|
||||
user_role_from_sso = get_nested_value(response, generic_user_role_attribute_name)
|
||||
user_role: Optional[LitellmUserRoles] = None
|
||||
if user_role_from_sso is not None:
|
||||
role = get_litellm_user_role(user_role_from_sso)
|
||||
if role is not None:
|
||||
user_role = role
|
||||
verbose_proxy_logger.debug(
|
||||
f"Found valid LitellmUserRoles '{role.value}' from SSO attribute '{generic_user_role_attribute_name}'"
|
||||
)
|
||||
|
||||
return CustomOpenID(
|
||||
id=get_nested_value(response, generic_user_id_attribute_name),
|
||||
display_name=get_nested_value(
|
||||
|
|
@ -287,7 +302,7 @@ def generic_response_convertor(
|
|||
last_name=get_nested_value(response, generic_user_last_name_attribute_name),
|
||||
provider=get_nested_value(response, generic_provider_attribute_name),
|
||||
team_ids=all_teams,
|
||||
user_role=None,
|
||||
user_role=user_role,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2438,6 +2438,70 @@ class TestGenericResponseConvertorNestedAttributes:
|
|||
assert result.display_name == "user-sub-123" # Top-level attribute works
|
||||
|
||||
|
||||
class TestGenericResponseConvertorUserRole:
|
||||
"""Test generic_response_convertor user role extraction from SSO token"""
|
||||
|
||||
def test_generic_response_convertor_extracts_valid_user_role(self):
|
||||
"""
|
||||
Test that generic_response_convertor extracts a valid LiteLLM user role
|
||||
from the SSO token using the GENERIC_USER_ROLE_ATTRIBUTE env var.
|
||||
"""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
from litellm.proxy.management_endpoints.ui_sso import generic_response_convertor
|
||||
|
||||
mock_jwt_handler = MagicMock(spec=JWTHandler)
|
||||
mock_jwt_handler.get_team_ids_from_jwt.return_value = []
|
||||
|
||||
sso_response = {
|
||||
"preferred_username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"sub": "Test User",
|
||||
"role": "proxy_admin",
|
||||
}
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"GENERIC_USER_ROLE_ATTRIBUTE": "role"},
|
||||
):
|
||||
result = generic_response_convertor(
|
||||
response=sso_response,
|
||||
jwt_handler=mock_jwt_handler,
|
||||
sso_jwt_handler=None,
|
||||
)
|
||||
|
||||
assert isinstance(result, CustomOpenID)
|
||||
assert result.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
|
||||
def test_generic_response_convertor_ignores_invalid_user_role(self):
|
||||
"""
|
||||
Test that generic_response_convertor ignores invalid role values
|
||||
and sets user_role to None.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.ui_sso import generic_response_convertor
|
||||
|
||||
mock_jwt_handler = MagicMock(spec=JWTHandler)
|
||||
mock_jwt_handler.get_team_ids_from_jwt.return_value = []
|
||||
|
||||
sso_response = {
|
||||
"preferred_username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"role": "invalid_role_value",
|
||||
}
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"GENERIC_USER_ROLE_ATTRIBUTE": "role"},
|
||||
):
|
||||
result = generic_response_convertor(
|
||||
response=sso_response,
|
||||
jwt_handler=mock_jwt_handler,
|
||||
sso_jwt_handler=None,
|
||||
)
|
||||
|
||||
assert isinstance(result, CustomOpenID)
|
||||
assert result.user_role is None
|
||||
|
||||
|
||||
class TestGetGenericSSORedirectParams:
|
||||
"""Test _get_generic_sso_redirect_params state parameter priority handling"""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue