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 7 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.
| with no `type`, the filter never matched, and every such PATCH appended a | ||
| duplicate email instead of updating the address in place. |
There was a problem hiding this comment.
I thought previously the PATCH just errored? How did it appent a duplicate email?
| res.status `shouldMatchInt` 200 | ||
| -- The provisioned email propagates end-to-end: SCIM GET reflects it and, | ||
| -- with validation disabled, it is active in Brig. | ||
| eventually $ do |
There was a problem hiding this comment.
Please explain the need for this eventually in a comment.
| -- @.value@. The proof is the new value and echoed @type = "work"@ below -- a | ||
| -- create-on-absent append would be collapsed back to the OLD address by | ||
| -- 'scimEmailsToEmailAddress', failing the value assertion. | ||
| eventually $ do |
There was a problem hiding this comment.
Also this eventually needs an explanation, it would be nice if we try to avoid these, but if something is genuinely async, sure it might make sense, but maybe there is some event (like the team admin getting a team member join event) which can be used for synchronizing.
| 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.
| scimEmailsToEmailAddress es | ||
| | Prelude.length primaries > 1 = | ||
| Left "More than one email is marked as primary; RFC 7643 §2.4 allows at most one." | ||
| | otherwise = Right (pickFirst primaries <|> pickFirst es) |
There was a problem hiding this comment.
nit: IMO this is faster and also looks neater. And we can remove the pickFirst function.
| scimEmailsToEmailAddress es | |
| | Prelude.length primaries > 1 = | |
| Left "More than one email is marked as primary; RFC 7643 §2.4 allows at most one." | |
| | otherwise = Right (pickFirst primaries <|> pickFirst es) | |
| scimEmailsToEmailAddress es = | |
| case primaries of | |
| [] -> Right Nothing | |
| [primaryEmail] -> Right . Just . unEmailAddress $ value primaryEmail | |
| _ -> Left "More than one email is marked as primary; RFC 7643 §2.4 allows at most one." |
https://wearezeta.atlassian.net/browse/WPB-23434
Checklist
changelog.d