WPB-23434: Support SCIM PATCH of multi-valued emails attribute - #5419
WPB-23434: Support SCIM PATCH of multi-valued emails attribute#5419blackheaven wants to merge 21 commits into
Conversation
a0fb7f7 to
e4acc08
Compare
akshaymankar
left a comment
There was a problem hiding this comment.
Looks good overall. Two things I would request:
- Document somewhere that Entra is acting wrong or fix
applyUserOperationso it doesn't rewriteAddtoReplacein case of multi-values and complex attributes. - The integration test should be in the new suite.
Other comments are nits.
1715cd6 to
709b260
Compare
Implement value-path PATCH (e.g. `emails[type eq "work"].value`) for the `emails` multi-valued attribute in hscim's applyUserOperation, which Entra and other IdPs use to provision user emails. Any value-path target was previously rejected with "can not lens into multi-valued attributes yet". Supports the value/type/primary sub-attributes and whole-entry paths; other multi-valued attributes remain unsupported. Also fix email propagation in Spar: an email-only PATCH does not change externalId, so updateVsuUref (the only update-path route to validateEmail -> Brig.updateEmail, gated on externalId change) never ran, and GET reconstructs the email from Brig -- so the change was a silent no-op. Add an explicit email-diff validateEmail in updateValidScimUser, gated on externalId being unchanged to avoid double-validation with updateVsuUref.
Handle Add on emails[...] value-path explicitly instead of rewriting to Replace: whole-entry Add appends (concat semantics) while sub-attribute Add delegates to the Replace path. Give ValuePath named record fields and drop the redundant valuePathFilter helper. Correct the create-on-absent NOTE to cite Entra's documented Add behaviour. Move the end-to-end email PATCH test to the new integration suite (patchScimUser helper + testSparPatchEmailValuePath) and remove the deprecated copy. Add hscim unit tests for the Add behaviour.
spar never persists the SCIM email 'type': synthesizeScimUser returns every email with typ=Nothing, so a value-path filter emails[type eq "work"] can never match an existing email. The original test created a user WITH an email, so the Add hit create-on-absent and appended a second entry; scimEmailsToEmailAddress (pickFirst) then returned the old email, so no email diff fired and Brig was never updated. Mirror the proven deprecated test instead: create an email-less SAML user, disable validateSAMLemails so the provisioned email auto-activates, and assert the Add on emails[type eq "work"].value provisions a work email that propagates to Brig. (Add delegates to the Replace path for the .value sub-attribute, so this is behaviour-equivalent to the prior Replace test.)
scimEmailsToEmailAddress now returns Either, rejecting (Left) when more than one email is marked primary (RFC 7643 §2.4 violation). spar's validateScimUser' turns the Left into a SCIM 400, covering POST/PUT/PATCH (PATCH re-validates via putUser). Previously spar silently first-wins, masking client-side misconfiguration. Other inputs (zero or one primary) are unchanged.
synthesizeScimUser now echoes type = Just "work" instead of Nothing. spar/brig store a single address with no type, but Entra PATCHes with a value-path filter emails[type eq "work"]; without an echoed type the filter never matched and such PATCHes appended a duplicate email rather than updating the address in place. Echoing the canonical type (RFC 7643 sec 4.1.2) at the single synthesis point makes the filter match across create/GET/PATCH. Adds testSparPatchEmailValuePathInPlace (in-place update), corrects the create-on-absent test comment, and adds a changelog fragment.
Follow-up to 1406a78 (synthesizeScimUser now echoes type = Just "work" instead of Nothing): update all SCIM email test-expectations to expect type = "work", since every GET/PUT/PATCH response now materializes it. integration framework (Test/Spar.hs): add a scimWorkEmail helper and route the 13 full-email-object assertions/inputs that compare to a response through it. PUT inputs never round-tripped to a response are left typeless, preserving coverage that the server synthesizes the type when omitted. spar test-integration: flip the two expected-email constructions (Util/Scim.hs setDefaultRoleAndEmailsIfEmpty, APISpec.hs scim-user-search) from Nothing to Just "work". Test-only; no production code.
7a95f96 to
f88dea5
Compare
akshaymankar
left a comment
There was a problem hiding this comment.
Looks good, but needs some explanation for the eventually use and I just want to be sure if we're changing APIs it is well tested and is very visible in release notes.
| resp.json %. "emails" `shouldMatch` (toJSON [object ["value" .= email]]) | ||
| resp.json %. "emails" `shouldMatch` (toJSON [scimWorkEmail email]) |
There was a problem hiding this comment.
Hmm, this looks like an API change, we must add an entry to release notes I think to warn people.
Btw, have we tested this with different setups? I'm scared this will break some customers.
There was a problem hiding this comment.
Done: added changelog.d/1-api-changes/WPB-23434-scim-emails-now-echo-type (6cce05f), flagging that SCIM user resources now include type: "work" on emails and that clients doing strict equality on the emails array will see the extra member. On testing coverage: the PR covers SCIM PUT/PATCH round-trips (create-on-absent, in-place update, unchanged-email PUT), Brig email activation/propagation, and the zero/one/multiple primary cases. Microsoft Entra ID is the only known consumer of emails[type eq ...] value-path filters; if you know of others we should test against, I'm happy to add cases.
Co-authored-by: Akshay Mankar <axeman@mailbox.org>
Document why the two eventually blocks in Test/Spar.hs are needed: the PATCH 200 precedes the asynchronous email re-provisioning in Brig, and there is no synchronizing event for SCIM-provisioned users (same pattern as the deprecated suite's specEmailValidation). Clarify in the bug-fix changelog that the old behavior appended a duplicate (RFC 7644 create-on-absent) rather than erroring, and add an api-changes entry for the echoed emails type field.
6cce05f to
931f486
Compare
scimEmailsToEmailAddress returned Nothing when no emails entry had primary = true, contradicting its documented selection rule (primary, else first entry). Fall back to the first entry instead.
03149a2 to
cffcf87
Compare
Instead of hardcoding type="work" when synthesizing SCIM users, spar now persists the type and primary sub-attributes of the stored email entry in spar.scim_user_times (migration V23) and echoes them verbatim. Users provisioned without metadata echo neither field (strict round-trip), so value-path filters only match when the IdP actually supplied them. - hscim: new scimEmailsToEmail exposes the selected Email entry (address + metadata); scimEmailsToEmailAddress refactored on top of it. - wire-api: ValidScimUser gains emailType/emailPrimary (included in Eq, so type-only/primary-only PATCHes become effective). - wire-subsystems: ScimUserTimesStore.Write gains the two metadata args; Read/ReadMulti return a ScimUserTimes record. - spar: validateScimUser' captures client-sent metadata; synthesizeStoredUser reads (or, at creation, forces) it; createValidScimUserSpar no longer upserts redundantly; updateValidScimUser persists new metadata.
- Regenerate cassandra-schema.cql with the two new scim_user_times columns. - Integration tests: randomScimUserWithEmailAndMeta, scimStoredEmail (strict echo), reworked value-path tests (stored type 'home'), new testSparScimEmailMetaRoundTrip (work+primary round-trip, Okta-style primary-filter in-place update, strict no-metadata echo); shared scimAddPatchOp helper. - spar test-integration: metadata round-trip spec incl. legacy-row simulation; expectations updated for strict echo. - Review fixes: bound email type length (64 chars) at validateScimUser'; drop dead storedUser param of createValidScimUserSpar; simplify synthesizeStoredUser metadata/writeState; document mEmailMeta precondition; ScimUserTimes derives (Eq, Show); changelog wording.
Add testSparScimEmailTypeNonWorkEcho to the integration suite (covers "home" and "other": POST 201 + exact GET echo) and a matching spec in spar's UserSpec GET /Users/:id block, proving no normalization to "work".
43d090d to
9d84951
Compare
…d (RFC 7643 2.5, RFC 7644 3.5.2.2) - PatchOp: keep explicit "value": null distinct from an absent value member - User: remove emails[...].type/.primary unassigns the field and keeps the entry; remove/null on .value is a 400; filterless remove emails clears all entries; replace with null unassigns Maybe-typed sub-attributes - unit + spar integration tests; changelog
https://wearezeta.atlassian.net/browse/WPB-23434
Checklist
changelog.d