mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-15 10:24:33 +00:00
[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:
@@ -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()
|
||||
Reference in New Issue
Block a user