diff --git a/litellm/proxy/management_endpoints/scim/scim_errors.py b/litellm/proxy/management_endpoints/scim/scim_errors.py new file mode 100644 index 00000000000..9e09f6e095e --- /dev/null +++ b/litellm/proxy/management_endpoints/scim/scim_errors.py @@ -0,0 +1,13 @@ +from fastapi import HTTPException + + +class ScimUserAlreadyExists(HTTPException): + """ + Exception raised when a user already exists in the database. + """ + def __init__(self, message: str, scim_type: str = "uniqueness"): + super().__init__(status_code=409, detail=message) + self.message = message + self.scim_type = scim_type + self.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"] + \ No newline at end of file diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 65dae40fa01..e6f7ed012cf 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -17,6 +17,7 @@ from fastapi import ( Request, Response, ) +from prisma.types import HttpConfig from litellm._logging import verbose_proxy_logger from litellm.litellm_core_utils.safe_json_dumps import safe_dumps @@ -32,9 +33,11 @@ from litellm.proxy._types import ( ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_endpoints.internal_user_endpoints import new_user +from litellm.proxy.management_endpoints.scim.scim_errors import ScimUserAlreadyExists from litellm.proxy.management_endpoints.scim.scim_transformations import ( ScimTransformations, ) +from litellm.proxy.management_endpoints.scim.utils import _extract_error_message from litellm.proxy.management_endpoints.team_endpoints import ( new_team, team_member_add, @@ -285,7 +288,6 @@ async def get_user( except Exception as e: raise handle_exception_on_proxy(e) - @scim_router.post( "/Users", response_model=SCIMUser, @@ -335,7 +337,8 @@ async def create_user( user=created_user ) return scim_user - + except HTTPException as e: # allow exceptions like SCIMUserAlreadyExists to be raised + raise e except Exception as e: raise handle_exception_on_proxy(e) diff --git a/litellm/proxy/management_endpoints/scim/utils.py b/litellm/proxy/management_endpoints/scim/utils.py new file mode 100644 index 00000000000..e899781b203 --- /dev/null +++ b/litellm/proxy/management_endpoints/scim/utils.py @@ -0,0 +1,20 @@ +from fastapi import HTTPException + + +async def _check_user_exists(prisma_client, user_name: str) -> bool: + """Check if user already exists by username""" + if not user_name: + return False + + existing_user = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": user_name} + ) + return existing_user is not None + + +def _extract_error_message(http_exception: HTTPException) -> str: + """Extract error message from HTTPException detail""" + if isinstance(http_exception.detail, dict): + return http_exception.detail.get("error", "User already exists") + return str(http_exception.detail) + 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 new file mode 100644 index 00000000000..8f7ec1f9b46 --- /dev/null +++ b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py @@ -0,0 +1,42 @@ +from unittest.mock import AsyncMock + +import pytest + +from litellm.proxy.management_endpoints.scim.scim_errors import ScimUserAlreadyExists +from litellm.proxy.management_endpoints.scim.scim_v2 import create_user +from litellm.types.proxy.management_endpoints.scim_v2 import ( + SCIMUser, + SCIMUserEmail, + SCIMUserName, +) + + +@pytest.mark.asyncio +async def test_create_user_existing_user_conflict(mocker): + """If a user already exists, create_user should raise ScimUserAlreadyExists""" + + scim_user = SCIMUser( + schemas=["urn:ietf:params:scim:schemas:core:2.0:User"], + userName="existing-user", + name=SCIMUserName(familyName="User", givenName="Existing"), + emails=[SCIMUserEmail(value="existing@example.com")], + ) + + mock_prisma = mocker.MagicMock() + + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma) + mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2._check_user_exists", + AsyncMock(return_value=True), + ) + mocked_new_user = mocker.patch( + "litellm.proxy.management_endpoints.scim.scim_v2.new_user", + AsyncMock(), + ) + + with pytest.raises(ScimUserAlreadyExists) as exc_info: + await create_user(user=scim_user) + + assert exc_info.value.status_code == 409 + assert "existing-user" in exc_info.value.message + mocked_new_user.assert_not_called()