Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
=========

[0.6.13] - 2026-07-27
---------------------

Fixed
^^^^^
- ``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>`_
- :class:`~scim2_models.ResourceType` ``endpoint`` is case-exact. :rfc:`7643` `erratum 8475 <https://www.rfc-editor.org/errata/eid8475>`_
- :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>`_
- :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.

[0.6.12] - 2026-04-13
---------------------

Expand Down
2 changes: 1 addition & 1 deletion samples/rfc7643-8.7.1-schema-enterprise_user.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"multiValued": false,
"description": "The URI of the SCIM resource representing the User's manager. REQUIRED.",
"required": true,
"caseExact": false,
"caseExact": true,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
Expand Down
4 changes: 2 additions & 2 deletions samples/rfc7643-8.7.1-schema-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"multiValued": false,
"description": "Identifier of the member of this Group.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "immutable",
"returned": "default",
"uniqueness": "none"
Expand All @@ -43,7 +43,7 @@
"multiValued": false,
"description": "The URI corresponding to a SCIM resource that is a member of this Group.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "immutable",
"returned": "default",
"uniqueness": "none"
Expand Down
6 changes: 3 additions & 3 deletions samples/rfc7643-8.7.1-schema-user.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"multiValued": false,
"description": "A fully qualified URL pointing to a page representing the User's online profile.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
Expand Down Expand Up @@ -562,7 +562,7 @@
"multiValued": false,
"description": "The identifier of the User's group.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
Expand All @@ -576,7 +576,7 @@
"multiValued": false,
"description": "The URI of the corresponding 'Group' resource to which the user belongs.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
Expand Down
2 changes: 1 addition & 1 deletion samples/rfc7643-8.7.2-schema-resource_type.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"multiValued": false,
"description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.",
"required": true,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "server"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"multiValued": false,
"description": "An HTTP-addressable URL pointing to the service provider's human-consumable help documentation.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
Expand Down Expand Up @@ -226,7 +226,7 @@
"multiValued": false,
"description": "An HTTP-addressable URL pointing to the authentication scheme's specification.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
Expand All @@ -240,7 +240,7 @@
"multiValued": false,
"description": "An HTTP-addressable URL pointing to the authentication scheme's usage documentation.",
"required": false,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
Expand Down
35 changes: 29 additions & 6 deletions scim2_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import get_origin

from pydantic import AliasGenerator
from pydantic import Base64Bytes
from pydantic import BaseModel as PydanticBaseModel
from pydantic import ConfigDict
from pydantic import FieldSerializationInfo
Expand All @@ -20,11 +21,13 @@
from pydantic_core import PydanticCustomError
from typing_extensions import Self

from scim2_models.annotations import CaseExact
from scim2_models.annotations import Mutability
from scim2_models.annotations import Required
from scim2_models.annotations import Returned
from scim2_models.context import Context
from scim2_models.exceptions import MutabilityException
from scim2_models.reference import Reference
from scim2_models.utils import UNION_TYPES
from scim2_models.utils import _find_field_name
from scim2_models.utils import _normalize_attribute_name
Expand Down Expand Up @@ -145,15 +148,35 @@ def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any:
"""
field_metadata = cls.model_fields[field_name].metadata

default_value = getattr(annotation_type, "_default", None)

def annotation_type_filter(item: Any) -> bool:
return isinstance(item, annotation_type)

field_annotation = next(
filter(annotation_type_filter, field_metadata), default_value
)
return field_annotation
field_annotation = next(filter(annotation_type_filter, field_metadata), None)
if field_annotation is not None:
return field_annotation

if annotation_type is CaseExact:
return cls._default_case_exact(field_name)

return getattr(annotation_type, "_default", None)

@classmethod
def _default_case_exact(cls, field_name: str) -> CaseExact:
"""Return the implicit case sensitivity of a field, based on its type.

:rfc:`RFC7643 §2.3.6 <7643#section-2.3.6>` and
:rfc:`§2.3.7 <7643#section-2.3.7>` state that binary and reference
values are case exact, whatever the schema representations of
:rfc:`§8.7 <7643#section-8.7>` say.
"""
root_type = cls.get_field_root_type(field_name)
if root_type == Base64Bytes:
return CaseExact.true

if isclass(root_type) and issubclass(root_type, Reference):
return CaseExact.true

return CaseExact.false

@classmethod
def get_field_root_type(cls, attribute_name: str) -> type | None:
Expand Down
3 changes: 2 additions & 1 deletion scim2_models/resources/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pydantic import Field

from ..annotations import CaseExact
from ..annotations import Mutability
from ..annotations import Required
from ..attributes import ComplexAttribute
Expand All @@ -18,7 +19,7 @@


class GroupMember(ComplexAttribute):
value: Annotated[str | None, Mutability.immutable] = None
value: Annotated[str | None, Mutability.immutable, CaseExact.true] = None
"""Identifier of the member of this Group."""

ref: Annotated[ # type: ignore[type-arg]
Expand Down
2 changes: 0 additions & 2 deletions scim2_models/resources/resource_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class SchemaExtension(ComplexAttribute):
Reference[URI] | None,
Mutability.read_only,
Required.true,
CaseExact.true,
] = Field(None, alias="schema")
"""The URI of a schema extension."""

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

Expand Down
24 changes: 21 additions & 3 deletions scim2_models/resources/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ def _to_python(
self,
reference_types: list[str] | None = None,
) -> type:
if self.value == self.reference and reference_types is not None:
if self.value == self.reference:
if reference_types == ["external"]:
return Reference[External]

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

if len(reference_types) == 1:
Expand Down Expand Up @@ -224,7 +226,7 @@ def _to_python(self) -> tuple[Any, Any] | None:
annotation = Annotated[
attr_type | None, # type: ignore
self.required,
self.case_exact,
self._implicit_case_exact(),
self.mutability,
self.returned,
self.uniqueness,
Expand All @@ -240,6 +242,22 @@ def _to_python(self) -> tuple[Any, Any] | None:

return annotation, field

def _implicit_case_exact(self) -> CaseExact:
"""Return the case sensitivity the built field must be annotated with.

Binary and reference values are case exact per
:rfc:`RFC7643 §2.3.6 <7643#section-2.3.6>` and
:rfc:`§2.3.7 <7643#section-2.3.7>`, unless the schema explicitly states
otherwise.
"""
if "case_exact" in self.model_fields_set:
return self.case_exact

if self.type in (self.Type.reference, self.Type.binary):
return CaseExact.true

return self.case_exact

def get_attribute(self, attribute_name: str) -> Optional["Attribute"]:
"""Find an attribute by its name."""
for sub_attribute in self.sub_attributes or []:
Expand Down
6 changes: 3 additions & 3 deletions scim2_models/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Type(str, Enum):
photo = "photo"
thumbnail = "thumbnail"

value: Annotated[Reference[External] | None, CaseExact.true] = None
value: Reference[External] | None = None
"""URL of a photo of the User."""

display: str | None = None
Expand Down Expand Up @@ -198,7 +198,7 @@ class Entitlement(ComplexAttribute):


class GroupMembership(ComplexAttribute):
value: Annotated[str | None, Mutability.read_only] = None
value: Annotated[str | None, Mutability.read_only, CaseExact.true] = None
"""The identifier of the User's group."""

ref: Annotated[
Expand Down Expand Up @@ -234,7 +234,7 @@ class Role(ComplexAttribute):


class X509Certificate(ComplexAttribute):
value: Annotated[Base64Bytes | None, CaseExact.true] = None
value: Base64Bytes | None = None
"""The value of an X.509 certificate."""

display: str | None = None
Expand Down
Loading
Loading