@@ -316,6 +316,16 @@ applyUserOperation ::
316316 User tag ->
317317 Operation ->
318318 m (User tag )
319+ applyUserOperation user (Operation Add (Just (IntoValuePath vp mSub)) (Just val)) =
320+ case vp of
321+ ValuePath (AttrPath _ attr _) _
322+ | attr == " emails" -> addEmailsValuePath user vp mSub val
323+ | otherwise ->
324+ throwError
325+ ( badRequest
326+ InvalidPath
327+ (Just " multi-valued PATCH is only supported for 'emails'" )
328+ )
319329applyUserOperation user (Operation Add path value) = applyUserOperation user (Operation Replace path value)
320330applyUserOperation user (Operation Replace (Just (NormalPath (AttrPath _schema attr _subAttr))) (Just value)) =
321331 case attr of
@@ -383,19 +393,17 @@ applyUserOperation user (Operation Remove (Just (IntoValuePath vp _mSub)) _) =
383393-- attribute that Spar persists. Other multi-valued attributes
384394-- (@phoneNumbers@, @ims@, ...) remain unsupported and still fail as before.
385395--
386- -- NOTE on "create on absent": RFC 7644 §3.5.2.3 says a @Replace@ value-path
387- -- that matches nothing is a no-op. Entra, however, emits an @Add@ (rewritten to
388- -- @Replace@ in 'applyUserOperation') against @emails[type eq "work"].value@ to
389- -- provision the address, expecting the entry to be created if absent. Every
390- -- mainstream SCIM client/validator expects this create-on-absent behaviour for
391- -- the email value-path, so we deviate from the RFC here: when the filter is
392- -- @type eq <s>@ and no entry matches, we append
396+ -- NOTE on "create on absent": RFC 7644 §3.5.2.3 says a value-path @Replace@
397+ -- that matches nothing is a no-op. Microsoft Entra ID, however, provisions the
398+ -- email address with an @Add@ against @emails[type eq "work"].value@ (Entra uses
399+ -- @Add@ for both insert and update -- see
400+ -- <https://learn.microsoft.com/en-us/answers/questions/1693075/why-is-entra-id-sending-add-operations-instead-of>),
401+ -- expecting the entry to be created if absent. Both 'addEmailsValuePath' and the
402+ -- @Replace@ path therefore route the @.value@ sub-attribute through
403+ -- 'replaceEmailValue', which deviates from the RFC: when the filter is
404+ -- @type eq <s>@ and no entry matches, it appends
393405-- @Email { typ = Just s, value = newVal, primary = Nothing }@.
394406
395- -- | The 'Filter' embedded in a 'ValuePath'.
396- valuePathFilter :: ValuePath -> Filter
397- valuePathFilter (ValuePath _ flt) = flt
398-
399407-- | Textual form of an 'Email' address, for string comparison.
400408emailValueText :: Email -> Text
401409emailValueText (Email _ addr _) =
@@ -472,6 +480,30 @@ decodeEmails val = case fromJSON val of
472480 Success (es' :: [Email ]) -> pure es'
473481 _ -> (: [] ) <$> resultToScimError (fromJSON val)
474482
483+ -- | Handle an @Add@ on an @emails[...]@ value-path.
484+ --
485+ -- For the single-valued email sub-attributes (@.value@, @.type@, @.primary@) an
486+ -- @Add@ coincides with a @Replace@ (RFC 7644 §3.5.2.3): it sets the
487+ -- sub-attribute and, for @.value@, creates the entry on absent via
488+ -- 'replaceEmailValue'. For a whole-entry @Add@ (no sub-attribute) the value-path
489+ -- filter is intentionally ignored and the new entries are /appended/ rather than
490+ -- overwriting matches -- the concat semantics that distinguish @Add@ from
491+ -- @Replace@ for multi-valued attributes (where @Replace@ narrows the target set
492+ -- via the filter).
493+ addEmailsValuePath ::
494+ (MonadError ScimError m ) =>
495+ User tag ->
496+ ValuePath ->
497+ Maybe SubAttr ->
498+ Value ->
499+ m (User tag )
500+ addEmailsValuePath user vp mSub val =
501+ case mSub of
502+ Just _ -> replaceEmailsValuePath user vp mSub val
503+ Nothing -> do
504+ newEmails <- decodeEmails val
505+ pure user {emails = emails user <> newEmails}
506+
475507-- | Handle a @Replace@ on an @emails[...]@ value-path.
476508replaceEmailsValuePath ::
477509 (MonadError ScimError m ) =>
0 commit comments