Compare values#
Use this guide when a service must compare strings in another way than scim2-models does by
default, for instance to apply the PRECIS rules that RFC7644 §5 requires
for userName and password. See Comparing values for what the default does
and why.
Write a comparison key#
A comparison key is a function with two arguments: the
AttributeBinding of an attribute, and a string value of that attribute.
It returns the string that comparisons use in place of the value, such as the value in
lowercase. Two values are equal when the key returns the same string for both. Set the function
on comparison_key:
>>> from unicodedata import normalize
>>> from scim2_models import ScimFilter, ScimPolicy, User, default_comparison_key
>>> def compatibility_key(binding, value):
... return default_comparison_key(binding, normalize("NFKC", value))
>>> policy = ScimPolicy(comparison_key=compatibility_key)
>>> user = User(user_name="bjensen")
>>> with policy:
... ScimFilter[User]('userName eq "BJENSEN"').match(user)
True
The key must be deterministic, have no side effects, and return its input unchanged when given
its own output. Fall back on default_comparison_key() for the attributes the
key does not handle.
Apply PRECIS to usernames and passwords#
The precis-i18n library implements the profiles of RFC 8265. Pick the profile from the URN of the attribute:
>>> from precis_i18n import get_profile
>>> USER = "urn:ietf:params:scim:schemas:core:2.0:User"
>>> PROFILES = {
... f"{USER}:userName": get_profile("UsernameCaseMapped"),
... f"{USER}:password": get_profile("OpaqueString"),
... }
>>> def precis_key(binding, value):
... profile = PROFILES.get(binding.urn)
... if profile is None:
... return default_comparison_key(binding, value)
... return profile.enforce(value)
>>> precis = ScimPolicy(comparison_key=precis_key)
UsernameCaseMapped maps fullwidth letters to ASCII, which the default key does not:
>>> with precis:
... ScimFilter[User]('userName eq "BJENSEN"').match(user)
True
It also refuses strings, such as a username holding a space. enforce raises a
UnicodeEncodeError, which is a ValueError, so a request that writes such a string
is refused with invalidValue:
>>> from scim2_models import Context
>>> payload = {"schemas": [USER], "userName": "Barbara Jensen"}
>>> User.model_validate(
... payload, scim_ctx=Context.RESOURCE_CREATION_REQUEST, scim_policy=precis
... )
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for User
...
A filter comparing such a string matches nothing, except with ne.
Use the policy in a service#
Give the policy to the ScimProvider of the service, so that every request
runs under it. scim2-server applies the key to its filters, sorting
and uniqueness checks.
Changing the key of a service that already holds data has two effects:
Values that were distinct can become equal. Under the default key,
BJENSENandbjensenare two userNames. Under PRECIS, they are the same one, and the two users that hold them break the uniqueness ofuserName.Stored values can become refused. A stored string the new key refuses is equal to no value. A filter does not find it, and a removal by value does not remove it. Only replacing the whole attribute changes it.
A storage that saves the string returned by the key next to each value, to filter and sort in its database, must compute these strings again.