Skip to content

Commit 78767df

Browse files
committed
fix: only allow one primary complex attribute value to be true
1 parent b7be1d2 commit 78767df

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
[0.4.3] - Unreleased
5+
--------------------
6+
7+
Fixed
8+
^^^^^
9+
- Only allow one primary complex attribute value to be true. :issue:`10`
10+
411
[0.4.2] - 2025-08-05
512
--------------------
613

scim2_models/base.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,55 @@ def check_replacement_request_mutability(
355355
cls._check_mutability_issues(original, obj)
356356
return obj
357357

358+
@model_validator(mode="after")
359+
def check_primary_attribute_uniqueness(self, info: ValidationInfo) -> Self:
360+
"""Validate that only one attribute can be marked as primary in multi-valued lists.
361+
362+
Per RFC 7643 Section 2.4: The primary attribute value 'true' MUST appear no more than once.
363+
"""
364+
from scim2_models.attributes import MultiValuedComplexAttribute
365+
366+
scim_context = info.context.get("scim") if info.context else None
367+
if not scim_context:
368+
return self
369+
370+
for field_name in self.__class__.model_fields:
371+
# Check if field is multi-valued (list type)
372+
if not self.get_field_multiplicity(field_name):
373+
continue
374+
375+
field_value = getattr(self, field_name)
376+
if field_value is None:
377+
continue
378+
379+
# Check if items in the list have a 'primary' attribute
380+
element_type = self.get_field_root_type(field_name)
381+
if (
382+
element_type is None
383+
or not isclass(element_type)
384+
or not issubclass(element_type, MultiValuedComplexAttribute)
385+
):
386+
continue
387+
388+
primary_count = sum(
389+
1
390+
for item in field_value
391+
if isinstance(item, PydanticBaseModel)
392+
and getattr(item, "primary", None) is True
393+
)
394+
395+
if primary_count > 1:
396+
raise PydanticCustomError(
397+
"primary_uniqueness_error",
398+
"Field '{field_name}' has {count} items marked as primary, but only one is allowed per RFC 7643",
399+
{
400+
"field_name": field_name,
401+
"count": primary_count,
402+
},
403+
)
404+
405+
return self
406+
358407
@classmethod
359408
def _check_mutability_issues(
360409
cls, original: "BaseModel", replacement: "BaseModel"

tests/test_user.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,65 @@ def test_full_user(load_sample):
124124
obj.meta.location
125125
== "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646"
126126
)
127+
128+
129+
def test_primary_attribute_validation_valid_cases():
130+
"""Test primary attribute validation for valid cases (0 or 1 primary)."""
131+
from scim2_models.context import Context
132+
133+
# Case 1: No primary attributes
134+
user_data = {
135+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
136+
"userName": "testuser",
137+
"emails": [
138+
{"value": "[email protected]", "type": "work"},
139+
{"value": "[email protected]", "type": "home"},
140+
],
141+
}
142+
user = User.model_validate(
143+
user_data, context={"scim": Context.RESOURCE_CREATION_REQUEST}
144+
)
145+
assert user.user_name == "testuser"
146+
147+
# Case 2: Exactly one primary attribute
148+
user_data_with_primary = {
149+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
150+
"userName": "testuser2",
151+
"emails": [
152+
{"value": "[email protected]", "type": "work", "primary": True},
153+
{"value": "[email protected]", "type": "home", "primary": False},
154+
],
155+
}
156+
user_with_primary = User.model_validate(
157+
user_data_with_primary, context={"scim": Context.RESOURCE_CREATION_REQUEST}
158+
)
159+
assert user_with_primary.emails[0].primary is True
160+
assert user_with_primary.emails[1].primary is False
161+
162+
163+
def test_primary_attribute_validation_invalid_case():
164+
"""Test primary attribute validation for invalid case (multiple primary)."""
165+
import pytest
166+
from pydantic import ValidationError
167+
168+
from scim2_models.context import Context
169+
170+
# Case: Multiple primary attributes (should fail)
171+
user_data_invalid = {
172+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
173+
"userName": "testuser3",
174+
"emails": [
175+
{"value": "[email protected]", "type": "work", "primary": True},
176+
{"value": "[email protected]", "type": "home", "primary": True},
177+
],
178+
}
179+
180+
with pytest.raises(ValidationError) as exc_info:
181+
User.model_validate(
182+
user_data_invalid, context={"scim": Context.RESOURCE_CREATION_REQUEST}
183+
)
184+
185+
error = exc_info.value.errors()[0]
186+
assert error["type"] == "primary_uniqueness_error"
187+
assert "emails" in error["ctx"]["field_name"]
188+
assert error["ctx"]["count"] == 2

0 commit comments

Comments
 (0)