Skip to content

Commit 2152df6

Browse files
committed
fix: make the password case-exact
1 parent 097dcf4 commit 2152df6

5 files changed

Lines changed: 24 additions & 3 deletions

File tree

‎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+
[Unreleased]
5+
------------
6+
7+
Security
8+
^^^^^^^^
9+
- The ``password`` of a :class:`~scim2_models.User` is case-exact, so ``password eq "SECRET"``
10+
no longer matches ``secret``. RFC 7643 declares it case-insensitive, but compares passwords
11+
by salted hash (:rfc:`RFC7643 §4.1.1 <7643#section-4.1.1>`), and the rules of
12+
:rfc:`RFC7644 §5 <7644#section-5>` keep the case of passwords.
13+
414
[0.9.0] - 2026-09-27
515
--------------------
616

‎samples/rfc7643-8.7.1-schema-user.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@
200200
"multiValued": false,
201201
"description": "The User's cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User'spassword.",
202202
"required": false,
203-
"caseExact": false,
203+
"caseExact": true,
204204
"mutability": "writeOnly",
205205
"returned": "never",
206206
"uniqueness": "none"

‎scim2_models/resources/user.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ class User(Resource[AnyExtension]):
295295
active: bool | None = None
296296
"""A Boolean value indicating the User's administrative status."""
297297

298-
password: Annotated[str | None, Mutability.write_only, Returned.never] = None
298+
password: Annotated[
299+
str | None, Mutability.write_only, Returned.never, CaseExact.true
300+
] = None
299301
"""The User's cleartext password."""
300302

301303
emails: list[Email] | None = None

‎tests/test_dynamic_resources.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ def test_make_user_model_from_schema(load_sample):
401401
== "The User's cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User'spassword."
402402
)
403403
assert User.get_field_annotation("password", Required) == Required.false
404-
assert User.get_field_annotation("password", CaseExact) == CaseExact.false
404+
assert User.get_field_annotation("password", CaseExact) == CaseExact.true
405405
assert User.get_field_annotation("password", Mutability) == Mutability.write_only
406406
assert User.get_field_annotation("password", Returned) == Returned.never
407407
assert User.get_field_annotation("password", Uniqueness) == Uniqueness.none

‎tests/test_filter_semantics.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,15 @@ def test_a_write_only_attribute_takes_an_equality_or_a_presence_test(filter_):
373373
ScimFilter[User](filter_)._validate_semantics()
374374

375375

376+
def test_a_password_is_compared_with_its_case():
377+
"""RFC7643 §4.1.1 compares passwords by salted hash, which keeps the case."""
378+
user = User(user_name="bjensen", password="Secret")
379+
380+
assert ScimFilter[User]('password eq "Secret"').match(user)
381+
assert not ScimFilter[User]('password eq "secret"').match(user)
382+
assert not ScimFilter[User]('password eq "SECRET"').match(user)
383+
384+
376385
@pytest.mark.parametrize("filter_", ['lock.code sw "x"', 'vault.label sw "x"'])
377386
def test_a_write_only_attribute_is_refused_through_a_complex_attribute(filter_):
378387
"""A write-only sub-attribute, or the sub-attribute of a write-only one, is refused."""

0 commit comments

Comments
 (0)