[SCIM] Add Error handling for existing user on SCIM (#11862)

* fix handling existing user on SCIM

* test scim v2 fixes
This commit is contained in:
Ishaan Jaff
2025-06-18 09:49:13 -07:00
committed by GitHub
parent c39b8f2178
commit ef336dcb38
4 changed files with 120 additions and 39 deletions
@@ -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"]
@@ -29,9 +29,14 @@ 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 (
_check_user_exists,
_extract_error_message,
)
from litellm.proxy.management_endpoints.team_endpoints import new_team
from litellm.proxy.utils import _premium_user_check, handle_exception_on_proxy
from litellm.types.proxy.management_endpoints.scim_v2 import *
@@ -151,7 +156,6 @@ async def get_user(
except Exception as e:
raise handle_exception_on_proxy(e)
@scim_router.post(
"/Users",
response_model=SCIMUser,
@@ -171,45 +175,47 @@ async def create_user(
try:
verbose_proxy_logger.debug("SCIM CREATE USER request: %s", user)
# Extract email from SCIM user
user_email = None
if user.emails and len(user.emails) > 0:
user_email = user.emails[0].value
# Check if user already exists
existing_user = None
if user.userName:
existing_user = await prisma_client.db.litellm_usertable.find_unique(
where={"user_id": user.userName}
)
if existing_user:
raise HTTPException(
status_code=409,
detail={"error": f"User already exists with username: {user.userName}"},
)
# Create user in database
# Extract user data
user_email = user.emails[0].value if user.emails else None
user_id = user.userName or str(uuid.uuid4())
created_user = await new_user(
data=NewUserRequest(
user_id=user_id,
user_email=user_email,
user_alias=user.name.givenName,
teams=[group.value for group in user.groups] if user.groups else None,
metadata={
"scim_metadata": LiteLLM_UserScimMetadata(
givenName=user.name.givenName,
familyName=user.name.familyName,
).model_dump()
},
auto_create_key=False,
),
)
scim_user = await ScimTransformations.transform_litellm_user_to_scim_user(
user=created_user
)
return scim_user
# Check for duplicate username
if await _check_user_exists(prisma_client, user.userName):
raise ScimUserAlreadyExists(
message=f"User already exists with username: {user.userName}"
)
# Attempt to create user
try:
created_user = await new_user(
data=NewUserRequest(
user_id=user_id,
user_email=user_email,
user_alias=user.name.givenName,
teams=[group.value for group in user.groups] if user.groups else None,
metadata={
"scim_metadata": LiteLLM_UserScimMetadata(
givenName=user.name.givenName,
familyName=user.name.familyName,
).model_dump()
},
auto_create_key=False,
),
)
except HTTPException as e:
# Convert duplicate email errors to SCIM 409
if e.status_code == 400 and "already exists" in str(e.detail):
raise ScimUserAlreadyExists(
message=_extract_error_message(e)
)
raise e
# Transform and return SCIM user
return await ScimTransformations.transform_litellm_user_to_scim_user(created_user)
except HTTPException:
raise # Let HTTPExceptions (including ScimUserAlreadyExists) propagate directly
except Exception as e:
raise handle_exception_on_proxy(e)
@@ -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)
@@ -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()