Skip to content

Bring PATCH in line with RFC 7644 - #170

Merged
azmeuk merged 7 commits into
mainfrom
patch
Sep 27, 2026
Merged

azmeuk merged 7 commits into
mainfrom
patch

Conversation

@azmeuk

@azmeuk azmeuk commented Sep 27, 2026

Copy link
Copy Markdown
Member

Fixes most of the gaps between PatchOp and RFC 7644 §3.5.2, and adds an option for Entra ID. The changelog has the full list.

Main changes:

  • add and replace on a complex attribute keep the sub-attributes the value leaves out.
  • add without a path adds to multi-valued attributes instead of replacing them.
  • read-only, immutable and required attributes are checked at every level.
  • ScimPolicy.unknown also applies to PATCH paths.
  • ScimPolicy.unmatched_path_filter can create the entry a filter describes, as Entra ID expects.

This can break existing code:

  • add on a filter that matches nothing now fails with noTarget.
  • replace without a value now fails with invalidValue.
  • a dumped PatchOperation keeps an explicit value: null.

SCIM dumps drop null values, so a replace that clears its target was sent without a value, and the server rejected it as malformed. The value is now kept when it was set, except on remove, which only uses the path (RFC 7644 §3.5.2.2).
Setting null under an unset complex attribute or extension created an empty object to hold it. The resource ended up with an empty object and was reported as modified. Nothing is created for null anymore.
A PatchOp with no operation, or with an op other than add, remove and replace, raised a pydantic error without a scimType, so a server could not turn it into a SCIM error. Both now raise invalidValue.
An add whose filter matched no entry did nothing and reported success. Microsoft Entra ID sends such requests, for example add emails[type eq "work"].value, and expects the entry to be created, so its data was silently lost. The operation is now rejected with noTarget, like replace (RFC 7644 §3.12).
Each operation is now split into attribute writes. A value that targets a resource or a complex attribute is written key by key, so the attributes it leaves out are kept (RFC 7644 §3.5.2.3), and a filter writes into the matching entries in place. The resource is then compared before and after the operation. This checks read-only, immutable and required attributes at every level, reports a change only when something changed, and fixes primary values. Only the attributes the operation touches are copied, so large multi-valued attributes stay cheap.
A path naming an undeclared attribute always failed with invalidPath, while the same attribute in an operation value was dropped under ignore and keep. The policy now applies to paths too: forbid returns invalidPath, and ignore and keep turn the operation into a no-op. An undeclared attribute inside a filter still returns invalidFilter.
Microsoft Entra ID sends add or replace on a filter that matches nothing, such as emails[type eq "work"].value on a user without a work email, and expects the entry to be created. By default scim2-models still returns noTarget, as RFC 7644 §3.5.2.3 requires for replace. With ScimPolicy.unmatched_path_filter set to create, the entry the filter describes (eq comparisons joined by and) is created, then the value is written into it. A value that would make the entry stop matching the filter is rejected.
@azmeuk
azmeuk merged commit 9b379bb into main Sep 27, 2026
70 of 72 checks passed
@azmeuk
azmeuk deleted the patch branch September 27, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant