From b9feb43dac4fd2c2085240c62ca13753b960efce Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 24 Jun 2025 13:22:56 -0700 Subject: [PATCH] [Bug Fix] SCIM - Ensure new user roles are applied (#12015) * SCIM fix new user roles * test_create_user_defaults_to_viewer * test_create_user_uses_default_internal_user_params_role * fix default user for SCIM * fix linting error --- .../management_endpoints/scim/scim_v2.py | 14 +++ .../scim/test_scim_v2_endpoints.py | 90 ++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index b88413f180e..1ef4cdc974d 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -18,6 +18,7 @@ from fastapi import ( Response, ) +import litellm from litellm._logging import verbose_proxy_logger from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.proxy._types import ( @@ -361,6 +362,18 @@ async def create_user( # Create user in database user_id = user.userName or str(uuid.uuid4()) metadata = _build_scim_metadata(user_data["given_name"], user_data["family_name"]) + + default_role: Optional[ + Literal[ + LitellmUserRoles.PROXY_ADMIN, + LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, + LitellmUserRoles.INTERNAL_USER, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, + ] + ] = LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + if litellm.default_internal_user_params: + default_role = litellm.default_internal_user_params.get("user_role") + new_user_request = NewUserRequest( user_id=user_id, user_email=user_data["user_email"], @@ -368,6 +381,7 @@ async def create_user( teams=user_data["teams"], metadata=metadata, auto_create_key=False, + user_role=default_role, ) # Check if user with email already exists and update if found diff --git a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py index aab700e2a72..f93728b90c3 100644 --- a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest from fastapi import HTTPException -from litellm.proxy._types import NewUserRequest, ProxyException +from litellm.proxy._types import LitellmUserRoles, NewUserRequest, ProxyException from litellm.proxy.management_endpoints.scim.scim_v2 import ( UserProvisionerHelpers, _handle_team_membership_changes, @@ -58,6 +58,94 @@ async def test_create_user_existing_user_conflict(mocker): mocked_new_user.assert_not_called() +@pytest.mark.asyncio +async def test_create_user_defaults_to_viewer(mocker, monkeypatch): + """If no role provided, new user should default to viewer""" + + scim_user = SCIMUser( + schemas=["urn:ietf:params:scim:schemas:core:2.0:User"], + userName="new-user", + name=SCIMUserName(familyName="User", givenName="New"), + emails=[SCIMUserEmail(value="new@example.com")], + ) + + mock_prisma_client = mocker.MagicMock() + mock_prisma_client.db = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + monkeypatch.setattr( + "litellm.default_internal_user_params", None, raising=False + ) + + mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception", + AsyncMock(return_value=mock_prisma_client), + ) + + new_user_mock = mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2.new_user", + AsyncMock(return_value=NewUserRequest(user_id="new-user")), + ) + + mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user", + AsyncMock(return_value=scim_user), + ) + + await create_user(user=scim_user) + + called_args = new_user_mock.call_args.kwargs["data"] + assert called_args.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + + +@pytest.mark.asyncio +async def test_create_user_uses_default_internal_user_params_role(mocker, monkeypatch): + """If role is set in default_internal_user_params, new user should use that role""" + + scim_user = SCIMUser( + schemas=["urn:ietf:params:scim:schemas:core:2.0:User"], + userName="new-user", + name=SCIMUserName(familyName="User", givenName="New"), + emails=[SCIMUserEmail(value="new@example.com")], + ) + + mock_prisma_client = mocker.MagicMock() + mock_prisma_client.db = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None) + + # Set default_internal_user_params with a specific role + default_params = { + "user_role": LitellmUserRoles.PROXY_ADMIN, + } + monkeypatch.setattr( + "litellm.default_internal_user_params", default_params, raising=False + ) + + mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception", + AsyncMock(return_value=mock_prisma_client), + ) + + new_user_mock = mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2.new_user", + AsyncMock(return_value=NewUserRequest(user_id="new-user")), + ) + + mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user", + AsyncMock(return_value=scim_user), + ) + + await create_user(user=scim_user) + + called_args = new_user_mock.call_args.kwargs["data"] + assert called_args.user_role == LitellmUserRoles.PROXY_ADMIN + + @pytest.mark.asyncio async def test_handle_existing_user_by_email_no_email(mocker): """Should return None when new_user_request has no email"""