Skip to content

Commit 1103415

Browse files
committed
fix: forbid PatchOp with zero or missing operations
1 parent c00a443 commit 1103415

File tree

3 files changed

+33
-1
lines changed

3 files changed

+33
-1
lines changed

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Fixed
99
- Attributes with ``None`` type are excluded from Schema generation.
1010
- Allow PATCH operations on resources and extensions root path.
1111
- Multiple ComplexAttribute do not inherit from MultiValuedComplexAttribute by default. :issue:`72` :issue:`73`
12+
- Forbid :class:`~scim2_models.PatchOp` with zero ``operations``.
1213

1314
[0.4.2] - 2025-08-05
1415
--------------------

scim2_models/messages/patch_op.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,17 @@ def __class_getitem__(
218218
"Operations", whose value is an array of one or more PATCH operations."""
219219

220220
@model_validator(mode="after")
221-
def validate_operations(self) -> Self:
221+
def validate_operations(self, info: ValidationInfo) -> Self:
222222
"""Validate operations against resource type metadata if available.
223223
224224
When PatchOp is used with a specific resource type (e.g., PatchOp[User]),
225225
this validator will automatically check mutability and required constraints.
226226
"""
227+
# RFC 7644: The body of an HTTP PATCH request MUST contain the attribute "Operations"
228+
scim_ctx = info.context.get("scim") if info.context else None
229+
if scim_ctx == Context.RESOURCE_PATCH_REQUEST and self.operations is None:
230+
raise ValueError(Error.make_invalid_value_error().detail)
231+
227232
resource_class = _get_resource_class(self)
228233
if resource_class is None or not self.operations:
229234
return self

tests/test_patch_op_validation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,29 @@ def test_remove_specific_value_invalid_field():
497497
# This should raise ValueError for invalid field name
498498
with pytest.raises(ValueError, match="no match|did not yield"):
499499
patch.patch(user)
500+
501+
502+
def test_patch_op_operations_attribute_required_in_patch_context():
503+
"""Test that Operations attribute is required in PATCH request context per RFC 7644."""
504+
# Operations attribute must be present in PATCH request context
505+
with pytest.raises(ValidationError, match="required value was missing"):
506+
PatchOp[User].model_validate(
507+
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]},
508+
context={"scim": Context.RESOURCE_PATCH_REQUEST},
509+
)
510+
511+
# Operations can be None when not in PATCH request context
512+
patch_op = PatchOp[User].model_validate(
513+
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]}
514+
)
515+
assert patch_op.operations is None
516+
517+
# Operations with at least one operation is valid in PATCH request context
518+
patch_op = PatchOp[User].model_validate(
519+
{
520+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
521+
"operations": [{"op": "add", "path": "userName", "value": "test"}],
522+
},
523+
context={"scim": Context.RESOURCE_PATCH_REQUEST},
524+
)
525+
assert len(patch_op.operations) == 1

0 commit comments

Comments
 (0)