Skip to content

Commit 0e65c5c

Browse files
ishaan-jaffstefan--
authored andcommitted
[Fix] Fix SCIM running patch operation case sensitivity (BerriAI#11335)
* fix: fix SCIM patch op * test: test SCIM patch op
1 parent 1ad68ff commit 0e65c5c

File tree

2 files changed

+73
-3
lines changed

2 files changed

+73
-3
lines changed

litellm/types/proxy/management_endpoints/scim_v2.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Dict, List, Literal, Optional, Union
22

33
from fastapi import HTTPException
4-
from pydantic import BaseModel, EmailStr
4+
from pydantic import BaseModel, EmailStr, field_validator
55

66

77
class LiteLLM_UserScimMetadata(BaseModel):
@@ -72,10 +72,20 @@ class SCIMListResponse(BaseModel):
7272

7373
# SCIM PATCH Operation Models
7474
class SCIMPatchOperation(BaseModel):
75-
op: Literal["add", "remove", "replace"]
75+
op: str
7676
path: Optional[str] = None
7777
value: Optional[Any] = None
7878

79+
@field_validator("op", mode="before")
80+
@classmethod
81+
def normalize_op(cls, v):
82+
if isinstance(v, str):
83+
v_lower = v.lower()
84+
if v_lower not in {"add", "remove", "replace"}:
85+
raise ValueError("op must be add, remove, or replace")
86+
return v_lower
87+
return v
88+
7989

8090
class SCIMPatchOp(BaseModel):
8191
schemas: List[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]

tests/test_litellm/proxy/management_endpoints/scim/test_scim_transformations.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
from litellm.proxy.management_endpoints.scim.scim_transformations import (
1919
ScimTransformations,
2020
)
21-
from litellm.types.proxy.management_endpoints.scim_v2 import SCIMGroup, SCIMUser
21+
from litellm.types.proxy.management_endpoints.scim_v2 import (
22+
SCIMGroup,
23+
SCIMPatchOp,
24+
SCIMPatchOperation,
25+
SCIMUser,
26+
)
2227

2328

2429
# Mock data
@@ -223,3 +228,58 @@ def test_get_scim_member_value(self):
223228
member_without_email = Member(user_id="user-456", user_email=None, role="user")
224229
result = ScimTransformations._get_scim_member_value(member_without_email)
225230
assert result == ScimTransformations.DEFAULT_SCIM_MEMBER_VALUE
231+
232+
233+
class TestSCIMPatchOperations:
234+
"""Test SCIM PATCH operation validation and case-insensitive handling"""
235+
236+
def test_scim_patch_operation_lowercase(self):
237+
"""Test that lowercase operations are accepted"""
238+
op = SCIMPatchOperation(op="add", path="members", value=[{"value": "user123"}])
239+
assert op.op == "add"
240+
241+
op = SCIMPatchOperation(op="remove", path='members[value eq "user123"]')
242+
assert op.op == "remove"
243+
244+
op = SCIMPatchOperation(op="replace", path="displayName", value="New Name")
245+
assert op.op == "replace"
246+
247+
def test_scim_patch_operation_uppercase(self):
248+
"""Test that uppercase operations are normalized to lowercase"""
249+
op = SCIMPatchOperation(op="ADD", path="members", value=[{"value": "user123"}])
250+
assert op.op == "add"
251+
252+
op = SCIMPatchOperation(op="REMOVE", path='members[value eq "user123"]')
253+
assert op.op == "remove"
254+
255+
op = SCIMPatchOperation(op="REPLACE", path="displayName", value="New Name")
256+
assert op.op == "replace"
257+
258+
def test_scim_patch_operation_mixed_case(self):
259+
"""Test that mixed case operations are normalized to lowercase"""
260+
op = SCIMPatchOperation(op="Add", path="members", value=[{"value": "user123"}])
261+
assert op.op == "add"
262+
263+
op = SCIMPatchOperation(op="Remove", path='members[value eq "user123"]')
264+
assert op.op == "remove"
265+
266+
op = SCIMPatchOperation(op="Replace", path="displayName", value="New Name")
267+
assert op.op == "replace"
268+
269+
def test_scim_patch_operation_with_optional_fields(self):
270+
"""Test SCIMPatchOperation with and without optional fields"""
271+
# Operation with all fields
272+
op_full = SCIMPatchOperation(
273+
op="Add",
274+
path="members",
275+
value=[{"value": "user123", "display": "User 123"}],
276+
)
277+
assert op_full.op == "add"
278+
assert op_full.path == "members"
279+
assert op_full.value == [{"value": "user123", "display": "User 123"}]
280+
281+
# Operation with minimal fields (only op is required)
282+
op_minimal = SCIMPatchOperation(op="Remove")
283+
assert op_minimal.op == "remove"
284+
assert op_minimal.path is None
285+
assert op_minimal.value is None

0 commit comments

Comments
 (0)