Skip to content

Commit 5145954

Browse files
authored
fix: binary and references are case exact by default (#147)
1 parent d5db17c commit 5145954

14 files changed

Lines changed: 170 additions & 39 deletions

doc/changelog.rst

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

4+
[0.6.13] - 2026-07-27
5+
---------------------
6+
7+
Fixed
8+
^^^^^
9+
- ``reference`` and ``binary`` attributes are case-exact, unless a schema explicitly states otherwise. :rfc:`7643` §2.3.6 and §2.3.7, `erratum 6001 <https://www.rfc-editor.org/errata/eid6001>`_
10+
- :class:`~scim2_models.ResourceType` ``endpoint`` is case-exact. :rfc:`7643` `erratum 8475 <https://www.rfc-editor.org/errata/eid8475>`_
11+
- :class:`~scim2_models.GroupMember` and :class:`~scim2_models.GroupMembership` ``value`` are case-exact, as they hold resource ``id`` values. :rfc:`7643` §3.1, in the spirit of `erratum 8472 <https://www.rfc-editor.org/errata/eid8472>`_
12+
- :meth:`~scim2_models.Resource.from_schema` no longer crashes on ``reference`` attributes missing the optional ``referenceTypes``, and reads them as :class:`~scim2_models.URI` references.
13+
414
[0.6.12] - 2026-04-13
515
---------------------
616

samples/rfc7643-8.7.1-schema-enterprise_user.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"multiValued": false,
8787
"description": "The URI of the SCIM resource representing the User's manager. REQUIRED.",
8888
"required": true,
89-
"caseExact": false,
89+
"caseExact": true,
9090
"mutability": "readWrite",
9191
"returned": "default",
9292
"uniqueness": "none"

samples/rfc7643-8.7.1-schema-group.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"multiValued": false,
2929
"description": "Identifier of the member of this Group.",
3030
"required": false,
31-
"caseExact": false,
31+
"caseExact": true,
3232
"mutability": "immutable",
3333
"returned": "default",
3434
"uniqueness": "none"
@@ -43,7 +43,7 @@
4343
"multiValued": false,
4444
"description": "The URI corresponding to a SCIM resource that is a member of this Group.",
4545
"required": false,
46-
"caseExact": false,
46+
"caseExact": true,
4747
"mutability": "immutable",
4848
"returned": "default",
4949
"uniqueness": "none"

samples/rfc7643-8.7.1-schema-user.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
"multiValued": false,
126126
"description": "A fully qualified URL pointing to a page representing the User's online profile.",
127127
"required": false,
128-
"caseExact": false,
128+
"caseExact": true,
129129
"mutability": "readWrite",
130130
"returned": "default",
131131
"uniqueness": "none"
@@ -562,7 +562,7 @@
562562
"multiValued": false,
563563
"description": "The identifier of the User's group.",
564564
"required": false,
565-
"caseExact": false,
565+
"caseExact": true,
566566
"mutability": "readOnly",
567567
"returned": "default",
568568
"uniqueness": "none"
@@ -576,7 +576,7 @@
576576
"multiValued": false,
577577
"description": "The URI of the corresponding 'Group' resource to which the user belongs.",
578578
"required": false,
579-
"caseExact": false,
579+
"caseExact": true,
580580
"mutability": "readOnly",
581581
"returned": "default",
582582
"uniqueness": "none"

samples/rfc7643-8.7.2-schema-resource_type.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"multiValued": false,
4747
"description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.",
4848
"required": true,
49-
"caseExact": false,
49+
"caseExact": true,
5050
"mutability": "readOnly",
5151
"returned": "default",
5252
"uniqueness": "server"

samples/rfc7643-8.7.2-schema-service_provider_configuration.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"multiValued": false,
1414
"description": "An HTTP-addressable URL pointing to the service provider's human-consumable help documentation.",
1515
"required": false,
16-
"caseExact": false,
16+
"caseExact": true,
1717
"mutability": "readOnly",
1818
"returned": "default",
1919
"uniqueness": "none"
@@ -226,7 +226,7 @@
226226
"multiValued": false,
227227
"description": "An HTTP-addressable URL pointing to the authentication scheme's specification.",
228228
"required": false,
229-
"caseExact": false,
229+
"caseExact": true,
230230
"mutability": "readOnly",
231231
"returned": "default",
232232
"uniqueness": "none"
@@ -240,7 +240,7 @@
240240
"multiValued": false,
241241
"description": "An HTTP-addressable URL pointing to the authentication scheme's usage documentation.",
242242
"required": false,
243-
"caseExact": false,
243+
"caseExact": true,
244244
"mutability": "readOnly",
245245
"returned": "default",
246246
"uniqueness": "none"

scim2_models/base.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import get_origin
77

88
from pydantic import AliasGenerator
9+
from pydantic import Base64Bytes
910
from pydantic import BaseModel as PydanticBaseModel
1011
from pydantic import ConfigDict
1112
from pydantic import FieldSerializationInfo
@@ -20,11 +21,13 @@
2021
from pydantic_core import PydanticCustomError
2122
from typing_extensions import Self
2223

24+
from scim2_models.annotations import CaseExact
2325
from scim2_models.annotations import Mutability
2426
from scim2_models.annotations import Required
2527
from scim2_models.annotations import Returned
2628
from scim2_models.context import Context
2729
from scim2_models.exceptions import MutabilityException
30+
from scim2_models.reference import Reference
2831
from scim2_models.utils import UNION_TYPES
2932
from scim2_models.utils import _find_field_name
3033
from scim2_models.utils import _normalize_attribute_name
@@ -145,15 +148,35 @@ def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any:
145148
"""
146149
field_metadata = cls.model_fields[field_name].metadata
147150

148-
default_value = getattr(annotation_type, "_default", None)
149-
150151
def annotation_type_filter(item: Any) -> bool:
151152
return isinstance(item, annotation_type)
152153

153-
field_annotation = next(
154-
filter(annotation_type_filter, field_metadata), default_value
155-
)
156-
return field_annotation
154+
field_annotation = next(filter(annotation_type_filter, field_metadata), None)
155+
if field_annotation is not None:
156+
return field_annotation
157+
158+
if annotation_type is CaseExact:
159+
return cls._default_case_exact(field_name)
160+
161+
return getattr(annotation_type, "_default", None)
162+
163+
@classmethod
164+
def _default_case_exact(cls, field_name: str) -> CaseExact:
165+
"""Return the implicit case sensitivity of a field, based on its type.
166+
167+
:rfc:`RFC7643 §2.3.6 <7643#section-2.3.6>` and
168+
:rfc:`§2.3.7 <7643#section-2.3.7>` state that binary and reference
169+
values are case exact, whatever the schema representations of
170+
:rfc:`§8.7 <7643#section-8.7>` say.
171+
"""
172+
root_type = cls.get_field_root_type(field_name)
173+
if root_type == Base64Bytes:
174+
return CaseExact.true
175+
176+
if isclass(root_type) and issubclass(root_type, Reference):
177+
return CaseExact.true
178+
179+
return CaseExact.false
157180

158181
@classmethod
159182
def get_field_root_type(cls, attribute_name: str) -> type | None:

scim2_models/resources/group.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from pydantic import Field
88

9+
from ..annotations import CaseExact
910
from ..annotations import Mutability
1011
from ..annotations import Required
1112
from ..attributes import ComplexAttribute
@@ -18,7 +19,7 @@
1819

1920

2021
class GroupMember(ComplexAttribute):
21-
value: Annotated[str | None, Mutability.immutable] = None
22+
value: Annotated[str | None, Mutability.immutable, CaseExact.true] = None
2223
"""Identifier of the member of this Group."""
2324

2425
ref: Annotated[ # type: ignore[type-arg]

scim2_models/resources/resource_type.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class SchemaExtension(ComplexAttribute):
2121
Reference[URI] | None,
2222
Mutability.read_only,
2323
Required.true,
24-
CaseExact.true,
2524
] = Field(None, alias="schema")
2625
"""The URI of a schema extension."""
2726

@@ -74,7 +73,6 @@ class ResourceType(Resource[Any]):
7473
Reference[URI] | None,
7574
Mutability.read_only,
7675
Required.true,
77-
CaseExact.true,
7876
] = Field(None, alias="schema")
7977
"""The resource type's primary/base schema URI."""
8078

scim2_models/resources/schema.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,13 @@ def _to_python(
101101
self,
102102
reference_types: list[str] | None = None,
103103
) -> type:
104-
if self.value == self.reference and reference_types is not None:
104+
if self.value == self.reference:
105105
if reference_types == ["external"]:
106106
return Reference[External]
107107

108-
if reference_types == ["uri"]:
108+
# 'referenceTypes' is not required by RFC7643 §7, and a
109+
# reference is a URI per §2.3.7.
110+
if not reference_types or reference_types == ["uri"]:
109111
return Reference[URI]
110112

111113
if len(reference_types) == 1:
@@ -224,7 +226,7 @@ def _to_python(self) -> tuple[Any, Any] | None:
224226
annotation = Annotated[
225227
attr_type | None, # type: ignore
226228
self.required,
227-
self.case_exact,
229+
self._implicit_case_exact(),
228230
self.mutability,
229231
self.returned,
230232
self.uniqueness,
@@ -240,6 +242,22 @@ def _to_python(self) -> tuple[Any, Any] | None:
240242

241243
return annotation, field
242244

245+
def _implicit_case_exact(self) -> CaseExact:
246+
"""Return the case sensitivity the built field must be annotated with.
247+
248+
Binary and reference values are case exact per
249+
:rfc:`RFC7643 §2.3.6 <7643#section-2.3.6>` and
250+
:rfc:`§2.3.7 <7643#section-2.3.7>`, unless the schema explicitly states
251+
otherwise.
252+
"""
253+
if "case_exact" in self.model_fields_set:
254+
return self.case_exact
255+
256+
if self.type in (self.Type.reference, self.Type.binary):
257+
return CaseExact.true
258+
259+
return self.case_exact
260+
243261
def get_attribute(self, attribute_name: str) -> Optional["Attribute"]:
244262
"""Find an attribute by its name."""
245263
for sub_attribute in self.sub_attributes or []:

0 commit comments

Comments
 (0)