From dd7711dd4bba65b2d05c1ff28d801183f309cf63 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 11:46:40 +0200 Subject: [PATCH 1/9] test --- integration/test/API/Brig.hs | 5 +++++ integration/test/Test/Spar.hs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 480bb781a1..3b11534584 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -990,6 +990,11 @@ getInvitationByCode user code = do req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "info"] submit "GET" (req & addQueryParams [("code", code)]) +deleteTeamInvitation :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +deleteTeamInvitation user tid iid = do + req <- baseRequest user Brig Versioned (joinHttpPath ["teams", tid, "invitations", iid]) + submit "DELETE" req + passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response passwordReset domain email = do req <- baseRequest domain Brig Versioned "password-reset" diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index dbdaf7eb0a..e5dc719771 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -131,6 +131,38 @@ testTeamInvitationWhenScimInvitationPending = do user %. "managed_by" `shouldMatch` "scim" user %. "status" `shouldMatch` "pending-invitation" +testScimReinviteAfterRevoke :: (HasCallStack) => App () +testScimReinviteAfterRevoke = do + let settings = + def + { brigCfg = + -- Controls when asynchronous cleanup removes expired SCIM pending accounts. + setField "optSettings.setExpiredUserCleanupTimeout" (3600 :: Int) + } + withModifiedBackend settings $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + token <- createScimToken owner def >>= getJSON 200 >>= (%. "token") >>= asString + + -- Create a SCIM user and let its invitation expire. Cleanup is deliberately + -- delayed so the expired pending account still exists at this point. + email <- randomEmail + externalId <- randomExternalId + scimUser <- randomScimUserWithEmail externalId email + scid <- createScimUser domain token scimUser >>= getJSON 201 >>= (%. "id") >>= asString + handle <- scimUser %. "userName" >>= asString + + -- assert that the SCIM handle is claimed + putHandle owner handle >>= assertStatus 409 + + -- cancel the invitation + void $ Brig.listInvitations owner tid >>= getJSON 200 >>= (%. "invitations") >>= asList >>= assertOne + Brig.deleteTeamInvitation owner tid scid >>= assertSuccess + void $ Brig.listInvitations owner tid >>= getJSON 200 >>= (%. "invitations") >>= shouldBeEmpty + + -- retry the invite should work + createScimUser domain token scimUser `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + testTeamInvitationWhenScimAccountExists :: (HasCallStack) => App () testTeamInvitationWhenScimAccountExists = do (owner, tid, _) <- createTeam OwnDomain 1 From de42991907cf57f00692cff0da03a996937a50c0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 11:46:48 +0200 Subject: [PATCH 2/9] clean up after invitation cancellation --- .../src/Wire/API/Routes/Internal/Spar.hs | 1 + .../wire-subsystems/src/Wire/SparAPIAccess.hs | 1 + .../src/Wire/SparAPIAccess/Rpc.hs | 10 +++++ .../Wire/MockInterpreters/SparAPIAccess.hs | 1 + services/brig/src/Brig/Team/API.hs | 41 ++++++++++++++++++- services/spar/src/Spar/API.hs | 17 ++++++++ services/spar/src/Spar/Scim/User.hs | 15 +++++++ 7 files changed, 84 insertions(+), 2 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index e2a23c2d1c..31233e07bd 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -32,6 +32,7 @@ type InternalAPI = "i" :> ( Named "i_status" ("status" :> Get '[JSON] NoContent) :<|> Named "i_delete_team" ("teams" :> Capture "team" TeamId :> DeleteNoContent) + :<|> Named "i_delete_scim_user" ("scim" :> "users" :> Capture "team" TeamId :> Capture "user" UserId :> DeleteNoContent) :<|> Named "i_put_sso_settings" ("sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent) :<|> Named "i_post_scim_user_info" ("scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo) :<|> Named "i_get_identity_providers" ("identity-providers" :> Capture "team" TeamId :> Get '[JSON] IdPList) diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs index b2df76bd01..0a6005162d 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs @@ -27,6 +27,7 @@ import Wire.API.User.IdentityProvider data SparAPIAccess m a where GetIdentityProviders :: TeamId -> SparAPIAccess m IdPList DeleteTeam :: TeamId -> SparAPIAccess m () + DeleteScimUser :: TeamId -> UserId -> SparAPIAccess m () LookupScimUserInfo :: UserId -> SparAPIAccess m ScimUserInfo makeSem ''SparAPIAccess diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs index fc08de1b81..a76692c5ff 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs @@ -49,6 +49,7 @@ interpretSparAPIAccessToRpc sparEndpoint = runInputConst sparEndpoint . \case GetIdentityProviders tid -> getIdentityProvidersImpl tid DeleteTeam tid -> deleteTeamImpl tid + DeleteScimUser tid uid -> deleteScimUserImpl tid uid LookupScimUserInfo uid -> lookupScimUserInfoImpl uid sparRequest :: @@ -93,6 +94,15 @@ deleteTeamImpl tid = do . paths ["i", "teams", toByteString' tid] . expect2xx +deleteScimUserImpl :: (Member (Input Endpoint) r, Member Rpc r) => TeamId -> UserId -> Sem r () +deleteScimUserImpl tid uid = do + void $ sparRequest delReq + where + delReq = + method DELETE + . paths ["i", "scim", "users", toByteString' tid, toByteString' uid] + . expect2xx + -- | Get the SCIM user info for a user. lookupScimUserInfoImpl :: ( Member (Error ParseException) r, diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs index 4122706e41..48f314d052 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs @@ -31,6 +31,7 @@ miniSparAPIAccess = interpret $ \case GetIdentityProviders tid -> Map.findWithDefault (IdPList []) tid <$> input DeleteTeam {} -> error "DeleteTeam not implemented in miniSparAPIAccess" + DeleteScimUser {} -> error "DeleteScimUser not implemented in miniSparAPIAccess" LookupScimUserInfo {} -> error "LookupScimUserInfo not implemented in miniSparAPIAccess" emptySparAPIAccess :: InterpreterFor SparAPIAccess r diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 48430fbeba..053e19388f 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -78,17 +78,22 @@ import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationStore qualified as Store import Wire.Sem.Concurrency +import Wire.SparAPIAccess (SparAPIAccess) +import qualified Wire.SparAPIAccess as SparAPIAccess import Wire.TeamInvitationSubsystem import Wire.TeamInvitationSubsystem.Interpreter (toInvitation) import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserKeyStore import Wire.UserPendingActivationStore (UserPendingActivationStore) +import qualified Wire.UserPendingActivationStore as UserPendingActivationStore import Wire.UserStore +import qualified Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem.Error servantAPI :: + forall p r. ( Member GalleyAPIAccess r, Member TeamInvitationSubsystem r, Member UserSubsystem r, @@ -98,7 +103,11 @@ servantAPI :: Member (Input (Local ())) r, Member (Error UserSubsystemError) r, Member IndexedUserStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member SparAPIAccess r, + Member UserStore r, + Member UserKeyStore r, + Member (UserPendingActivationStore p) r ) => ServerT TeamsAPI (Handler r) servantAPI = @@ -202,9 +211,16 @@ logInvitationRequest context action = pure (Right result) deleteInvitation :: + forall p r. ( Member InvitationStore r, Member (Error UserSubsystemError) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member SparAPIAccess r, + Member UserSubsystem r, + Member UserStore r, + Member UserKeyStore r, + Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r ) => UserId -> TeamId -> @@ -212,6 +228,27 @@ deleteInvitation :: Sem r () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] + mInvitation <- Store.lookupInvitation tid iid + for_ mInvitation $ \inv -> do + let scimUid = invitationIdToUserId iid + mUser <- getAccountNoFilter =<< qualifyLocal' scimUid + pendingScimUsers <- Store.lookupPendingScimUsers tid inv.email + for_ mUser $ \user -> + when + ( userId user == scimUid + && user.userTeam == Just tid + && user.userManagedBy == ManagedByScim + && user.userStatus == PendingInvitation + && userEmail user == Just inv.email + && scimUid `elem` pendingScimUsers + ) $ do + -- Remove Spar's external-id mapping before deleting the Brig account. + -- Otherwise a SCIM retry still sees the old external ID as owned. + SparAPIAccess.deleteScimUser tid scimUid + deleteKeyForUser scimUid (mkEmailKey inv.email) + UserPendingActivationStore.remove scimUid + UserStore.deleteUser user + Store.deletePendingScimUser tid inv.email scimUid Store.deleteInvitation tid iid listInvitations :: diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index f18e882b49..e0044721f2 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -263,6 +263,7 @@ apiINTERNAL :: Member IdPConfigStore r, Member (Error SparError) r, Member SAMLUserStore r, + Member ScimExternalIdStore r, Member ScimUserMetaStore r, Member (Logger (Msg -> Msg)) r, Member Random r, @@ -273,6 +274,7 @@ apiINTERNAL :: apiINTERNAL = Named @"i_status" internalStatus :<|> Named @"i_delete_team" internalDeleteTeam + :<|> Named @"i_delete_scim_user" internalDeleteScimUser :<|> Named @"i_put_sso_settings" internalPutSsoSettings :<|> Named @"i_post_scim_user_info" internalGetScimUserInfo :<|> Named @"i_get_identity_providers" idpGetAllByTeamId @@ -1132,6 +1134,21 @@ internalDeleteTeam teamId = do deleteTeam teamId pure NoContent +internalDeleteScimUser :: + ( Member BrigAPIAccess r, + Member ScimExternalIdStore r, + Member ScimUserMetaStore r, + Member SAMLUserStore r + ) => + TeamId -> + UserId -> + Sem r NoContent +internalDeleteScimUser teamId uid = do + BrigAPIAccess.getAccount WithPendingInvitations uid >>= \case + Just user | userTeam user == Just teamId -> deleteScimUserData teamId user + _ -> pure () + pure NoContent + internalPutSsoSettings :: ( Member DefaultSsoCode r, Member (Error SparError) r, diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 95e040bc66..c9c6e3a515 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -41,6 +41,7 @@ module Spar.Scim.User mkValidScimId, scimFindUserByExternalId, deleteScimUser, + deleteScimUserData, ) where @@ -899,6 +900,20 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = ScimExternalIdStore.delete stiTeam veid.validScimIdExternal lift $ ScimUserMetaStore.delete uid +deleteScimUserData :: + ( Member ScimExternalIdStore r, + Member ScimUserMetaStore r, + Member SAMLUserStore r + ) => + TeamId -> + User -> + Sem r () +deleteScimUserData teamId account = do + for_ (Intra.oldVeidFromBrigUser account) $ \veid -> do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete (userId account)) + ScimExternalIdStore.delete teamId veid.validScimIdExternal + ScimUserMetaStore.delete (userId account) + ---------------------------------------------------------------------------- -- Utilities From c0f6f5d2d629fee5d80534817f6bb75739b25df4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 11:56:56 +0200 Subject: [PATCH 3/9] simplification --- services/brig/src/Brig/Team/API.hs | 34 ++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 053e19388f..09121861c4 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -69,6 +69,7 @@ import Wire.API.Team.Size import Wire.API.User hiding (fromEmail) import Wire.AuthenticationSubsystem import Wire.BlockListStore +import Wire.ClientStore (ClientStore) import Wire.EmailSubsystem.Interpreter (renderInvitationUrl) import Wire.Error import Wire.Events (Events) @@ -77,18 +78,20 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationStore qualified as Store +import Wire.NotificationSubsystem (NotificationSubsystem) +import Wire.PropertySubsystem (PropertySubsystem) import Wire.Sem.Concurrency import Wire.SparAPIAccess (SparAPIAccess) -import qualified Wire.SparAPIAccess as SparAPIAccess +import Wire.SparAPIAccess qualified as SparAPIAccess import Wire.TeamInvitationSubsystem import Wire.TeamInvitationSubsystem.Interpreter (toInvitation) import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem +import Wire.UserGroupSubsystem (UserGroupSubsystem) import Wire.UserKeyStore import Wire.UserPendingActivationStore (UserPendingActivationStore) -import qualified Wire.UserPendingActivationStore as UserPendingActivationStore +import Wire.UserPendingActivationStore qualified as UserPendingActivationStore import Wire.UserStore -import qualified Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem.Error @@ -105,6 +108,13 @@ servantAPI :: Member IndexedUserStore r, Member TeamSubsystem r, Member SparAPIAccess r, + Member (Embed App.HttpClientIO) r, + Member NotificationSubsystem r, + Member ClientStore r, + Member PropertySubsystem r, + Member UserGroupSubsystem r, + Member Events r, + Member AuthenticationSubsystem r, Member UserStore r, Member UserKeyStore r, Member (UserPendingActivationStore p) r @@ -216,6 +226,14 @@ deleteInvitation :: Member (Error UserSubsystemError) r, Member TeamSubsystem r, Member SparAPIAccess r, + Member TinyLog r, + Member (Embed App.HttpClientIO) r, + Member NotificationSubsystem r, + Member ClientStore r, + Member PropertySubsystem r, + Member UserGroupSubsystem r, + Member Events r, + Member AuthenticationSubsystem r, Member UserSubsystem r, Member UserStore r, Member UserKeyStore r, @@ -241,14 +259,16 @@ deleteInvitation uid tid iid = do && user.userStatus == PendingInvitation && userEmail user == Just inv.email && scimUid `elem` pendingScimUsers - ) $ do + ) + $ do -- Remove Spar's external-id mapping before deleting the Brig account. -- Otherwise a SCIM retry still sees the old external ID as owned. SparAPIAccess.deleteScimUser tid scimUid - deleteKeyForUser scimUid (mkEmailKey inv.email) UserPendingActivationStore.remove scimUid - UserStore.deleteUser user - Store.deletePendingScimUser tid inv.email scimUid + -- Use the same complete deletion logic as the asynchronous user + -- deletion worker, but run it synchronously before the invitation is + -- removed so a replacement SCIM invitation can be created safely. + API.deleteAccount user Store.deleteInvitation tid iid listInvitations :: From ff8e05a63d4300c307748e52f9fd0e8360c1ca82 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 11:58:23 +0200 Subject: [PATCH 4/9] changelog --- changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke diff --git a/changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke b/changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke new file mode 100644 index 0000000000..29da72e51f --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke @@ -0,0 +1,3 @@ +Revoking a pending SCIM invitation now removes the associated Brig account and +Spar SCIM metadata synchronously, allowing the same SCIM user to be invited +again. From 1c375ecf4b327b64d699cfdd1e2b5bb8322b3ee0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 12:13:14 +0200 Subject: [PATCH 5/9] fix PR finding --- services/spar/src/Spar/API.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index e0044721f2..e73d9addef 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -1145,7 +1145,11 @@ internalDeleteScimUser :: Sem r NoContent internalDeleteScimUser teamId uid = do BrigAPIAccess.getAccount WithPendingInvitations uid >>= \case - Just user | userTeam user == Just teamId -> deleteScimUserData teamId user + Just user + | userTeam user == Just teamId + && userManagedBy user == ManagedByScim + && userStatus user == PendingInvitation -> + deleteScimUserData teamId user _ -> pure () pure NoContent From 97fbc9757b6f43236da0f7fa662f0f4fac9da639 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 12:13:25 +0200 Subject: [PATCH 6/9] rename changelog file --- .../{WPB-23177-scim-reinvite-after-revoke => WPB-18929} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/3-bug-fixes/{WPB-23177-scim-reinvite-after-revoke => WPB-18929} (100%) diff --git a/changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke b/changelog.d/3-bug-fixes/WPB-18929 similarity index 100% rename from changelog.d/3-bug-fixes/WPB-23177-scim-reinvite-after-revoke rename to changelog.d/3-bug-fixes/WPB-18929 From 61e3326536885882bbc4874cc79584653de4852f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 12:15:36 +0200 Subject: [PATCH 7/9] removed obsolete comment --- integration/test/Test/Spar.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index e5dc719771..d2a2f4770c 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -143,8 +143,6 @@ testScimReinviteAfterRevoke = do (owner, tid, _) <- createTeam domain 1 token <- createScimToken owner def >>= getJSON 200 >>= (%. "token") >>= asString - -- Create a SCIM user and let its invitation expire. Cleanup is deliberately - -- delayed so the expired pending account still exists at this point. email <- randomEmail externalId <- randomExternalId scimUser <- randomScimUserWithEmail externalId email From 95e6774c6304960e9c49cbf6c3dcbb2fcfedfe56 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 12:21:44 +0200 Subject: [PATCH 8/9] log --- services/spar/src/Spar/API.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index e73d9addef..6886106b97 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -1138,12 +1138,17 @@ internalDeleteScimUser :: ( Member BrigAPIAccess r, Member ScimExternalIdStore r, Member ScimUserMetaStore r, - Member SAMLUserStore r + Member SAMLUserStore r, + Member (Logger (Msg -> Msg)) r ) => TeamId -> UserId -> Sem r NoContent internalDeleteScimUser teamId uid = do + Logger.info $ + Log.msg ("Attempting to delete SCIM user data" :: String) + . Log.field "team" (idToText teamId) + . Log.field "user" (idToText uid) BrigAPIAccess.getAccount WithPendingInvitations uid >>= \case Just user | userTeam user == Just teamId From 556daa0d6cbe5fb695c87a8b28a07ed66bd32a09 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 12:34:59 +0200 Subject: [PATCH 9/9] fix PR finding --- services/brig/src/Brig/Team/API.hs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 09121861c4..ce88b50a63 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -247,17 +247,18 @@ deleteInvitation :: deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] mInvitation <- Store.lookupInvitation tid iid - for_ mInvitation $ \inv -> do - let scimUid = invitationIdToUserId iid - mUser <- getAccountNoFilter =<< qualifyLocal' scimUid - pendingScimUsers <- Store.lookupPendingScimUsers tid inv.email - for_ mUser $ \user -> + let scimUid = invitationIdToUserId iid + mUser <- getAccountNoFilter =<< qualifyLocal' scimUid + for_ mUser $ \user -> + for_ (userEmail user) $ \email -> do + pendingScimUsers <- Store.lookupPendingScimUsers tid email + let invitationMatches = maybe True (\inv -> inv.email == email) mInvitation when ( userId user == scimUid && user.userTeam == Just tid && user.userManagedBy == ManagedByScim && user.userStatus == PendingInvitation - && userEmail user == Just inv.email + && invitationMatches && scimUid `elem` pendingScimUsers ) $ do