diff --git a/changelog.d/6-federation/mls-welcome b/changelog.d/6-federation/mls-welcome
new file mode 100644
index 00000000000..0fee4bf7101
--- /dev/null
+++ b/changelog.d/6-federation/mls-welcome
@@ -0,0 +1 @@
+Support remote welcome messages
diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
index b6ca885338a..6b4ed3fd560 100644
--- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
+++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
@@ -14,12 +14,13 @@
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Wire.API.Federation.API.Galley where
import Data.Aeson (FromJSON, ToJSON)
-import Data.Id (ClientId, ConvId, UserId)
-import Data.Json.Util (Base64ByteString)
+import Data.Id
+import Data.Json.Util
import Data.Misc (Milliseconds)
import Data.Qualified
import Data.Range
@@ -59,6 +60,7 @@ type GalleyApi =
:<|> FedEndpoint "send-message" MessageSendRequest MessageSendResponse
:<|> FedEndpoint "on-user-deleted-conversations" UserDeletedConversationsNotification EmptyResponse
:<|> FedEndpoint "update-conversation" ConversationUpdateRequest ConversationUpdateResponse
+ :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest ()
data GetConversationsRequest = GetConversationsRequest
{ gcrUserId :: UserId,
@@ -253,3 +255,18 @@ data ConversationUpdateResponse
deriving
(ToJSON, FromJSON)
via (CustomEncoded ConversationUpdateResponse)
+
+newtype MLSWelcomeRecipient = MLSWelcomeRecipient {unMLSWelRecipient :: (UserId, ClientId)}
+ deriving stock (Generic)
+ deriving (Arbitrary) via (GenericUniform MLSWelcomeRecipient)
+ deriving (FromJSON, ToJSON) via CustomEncoded MLSWelcomeRecipient
+ deriving newtype (Show, Eq)
+
+data MLSWelcomeRequest = MLSWelcomeRequest
+ { mwrRawWelcome :: Base64ByteString,
+ -- | These are qualified implicitly by the target domain
+ mwrRecipients :: [MLSWelcomeRecipient]
+ }
+ deriving stock (Generic)
+ deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest)
+ deriving (FromJSON, ToJSON) via (CustomEncoded MLSWelcomeRequest)
diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs
index f1dc56c81eb..2e7187f6470 100644
--- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs
+++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs
@@ -144,7 +144,7 @@ type AccountAPI =
:> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile)
)
-type MLSAPI = GetClientByKeyPackageRef :<|> GetMLSClients
+type MLSAPI = GetClientByKeyPackageRef :<|> GetMLSClients :<|> MapKeyPackageRefs
type GetClientByKeyPackageRef =
Summary "Resolve an MLS key package ref to a qualified client ID"
@@ -171,6 +171,13 @@ type GetMLSClients =
'[Servant.JSON]
(Respond 200 "MLS clients" (Set ClientId))
+type MapKeyPackageRefs =
+ Summary "Insert bundle into the KeyPackage ref mapping. Only for tests."
+ :> "mls"
+ :> "key-package-refs"
+ :> ReqBody '[Servant.JSON] KeyPackageBundle
+ :> MultiVerb 'PUT '[Servant.JSON] '[RespondEmpty 204 "Mapping was updated"] ()
+
type GetVerificationCode =
Summary "Get verification code for a given email and action"
:> "users"
diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs
index b3eb4ff1f01..94c150661bb 100644
--- a/services/brig/src/Brig/API/Internal.hs
+++ b/services/brig/src/Brig/API/Internal.hs
@@ -108,7 +108,7 @@ ejpdAPI =
:<|> getConnectionsStatus
mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r)
-mlsAPI = getClientByKeyPackageRef :<|> getMLSClients
+mlsAPI = getClientByKeyPackageRef :<|> getMLSClients :<|> mapKeyPackageRefsInternal
accountAPI :: ServerT BrigIRoutes.AccountAPI (Handler r)
accountAPI = Named @"createUserNoVerify" createUserNoVerify
@@ -148,6 +148,12 @@ getMLSClients qusr ss = do
getKey :: MonadClient m => ClientId -> m (ClientId, Maybe LByteString)
getKey cid = (cid,) <$> Data.lookupMLSPublicKey (qUnqualified qusr) cid ss
+mapKeyPackageRefsInternal :: KeyPackageBundle -> Handler r ()
+mapKeyPackageRefsInternal bundle = do
+ wrapClientE $
+ for_ (kpbEntries bundle) $ \e ->
+ Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e)
+
getVerificationCode :: UserId -> VerificationAction -> (Handler r) (Maybe Code.Value)
getVerificationCode uid action = do
user <- wrapClientE $ Api.lookupUser NoPendingInvitations uid
diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs
index 0f39544d021..bbfd6642e85 100644
--- a/services/galley/src/Galley/API/Federation.hs
+++ b/services/galley/src/Galley/API/Federation.hs
@@ -14,6 +14,7 @@
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
+{-# LANGUAGE OverloadedStrings #-}
module Galley.API.Federation where
@@ -22,8 +23,8 @@ import Control.Lens (itraversed, (<.>))
import Data.ByteString.Conversion (toByteString')
import Data.Containers.ListUtils (nubOrd)
import Data.Domain (Domain)
-import Data.Id (ConvId, UserId)
-import Data.Json.Util (Base64ByteString (..))
+import Data.Id
+import Data.Json.Util
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map as Map
import Data.Map.Lens (toMapOf)
@@ -37,6 +38,7 @@ import Galley.API.Action
import Galley.API.Error
import qualified Galley.API.Mapping as Mapping
import Galley.API.Message
+import Galley.API.Push
import Galley.API.Util
import Galley.App
import qualified Galley.Data.Conversation as Data
@@ -88,6 +90,7 @@ federationSitemap =
:<|> Named @"send-message" sendMessage
:<|> Named @"on-user-deleted-conversations" onUserDeleted
:<|> Named @"update-conversation" updateConversation
+ :<|> Named @"mls-welcome" mlsSendWelcome
onConversationCreated ::
Members
@@ -523,3 +526,29 @@ instance
runError act >>= \case
Left _ -> throw (demote @err)
Right res -> pure res
+
+mlsSendWelcome ::
+ Members
+ '[ GundeckAccess,
+ Input (Local ()),
+ Input UTCTime
+ ]
+ r =>
+ Domain ->
+ F.MLSWelcomeRequest ->
+ Sem r ()
+mlsSendWelcome _origDomain (F.MLSWelcomeRequest b64RawWelcome rcpts) = do
+ loc <- input @(Local ())
+ now <- input @UTCTime
+ let rawWelcome = fromBase64ByteString b64RawWelcome
+ void $
+ runMessagePush loc Nothing $
+ foldMap (uncurry $ mkPush rawWelcome loc now) (F.unMLSWelRecipient <$> rcpts)
+ where
+ mkPush :: ByteString -> Local x -> UTCTime -> UserId -> ClientId -> MessagePush 'Broadcast
+ mkPush rawWelcome l time u c =
+ -- FUTUREWORK: use the conversation ID stored in the key package mapping table
+ let lcnv = qualifyAs l (Data.selfConv u)
+ lusr = qualifyAs l u
+ e = Event (qUntagged lcnv) (qUntagged lusr) time $ EdMLSWelcome rawWelcome
+ in newMessagePush l () Nothing defMessageMetadata (u, c) e
diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs
index f7d2fad7b26..b8445b62cbd 100644
--- a/services/galley/src/Galley/API/MLS/Welcome.hs
+++ b/services/galley/src/Galley/API/MLS/Welcome.hs
@@ -19,12 +19,14 @@ module Galley.API.MLS.Welcome (postMLSWelcome) where
import Control.Comonad
import Data.Id
+import Data.Json.Util
import Data.Qualified
import Data.Time
import Galley.API.MLS.KeyPackage
import Galley.API.Push
import Galley.Data.Conversation
import Galley.Effects.BrigAccess
+import Galley.Effects.FederatorAccess
import Galley.Effects.GundeckAccess
import Imports
import Polysemy
@@ -32,6 +34,8 @@ import Polysemy.Input
import Wire.API.Error
import Wire.API.Error.Galley
import Wire.API.Event.Conversation
+import Wire.API.Federation.API
+import Wire.API.Federation.API.Galley
import Wire.API.MLS.Credential
import Wire.API.MLS.Serialisation
import Wire.API.MLS.Welcome
@@ -39,6 +43,7 @@ import Wire.API.MLS.Welcome
postMLSWelcome ::
Members
'[ BrigAccess,
+ FederatorAccess,
GundeckAccess,
ErrorS 'MLSKeyPackageRefNotFound,
Input UTCTime
@@ -70,7 +75,8 @@ welcomeRecipients =
sendWelcomes ::
Members
- '[ GundeckAccess,
+ '[ FederatorAccess,
+ GundeckAccess,
Input UTCTime
]
r =>
@@ -102,5 +108,16 @@ sendLocalWelcomes con now rawWelcome lclients = do
e = Event (qUntagged lcnv) (qUntagged lusr) now $ EdMLSWelcome rawWelcome
in newMessagePush lclients () (Just con) defMessageMetadata (u, c) e
-sendRemoteWelcomes :: ByteString -> Remote [(UserId, ClientId)] -> Sem r ()
-sendRemoteWelcomes = undefined
+sendRemoteWelcomes ::
+ Members '[FederatorAccess] r =>
+ ByteString ->
+ Remote [(UserId, ClientId)] ->
+ Sem r ()
+sendRemoteWelcomes rawWelcome rClients = do
+ let req =
+ MLSWelcomeRequest
+ { mwrRawWelcome = Base64ByteString rawWelcome,
+ mwrRecipients = MLSWelcomeRecipient <$> tUnqualified rClients
+ }
+ rpc = fedClient @'Galley @"mls-welcome" req
+ void $ runFederated rClients rpc
diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs
index 8caf4f881de..a68d428dd83 100644
--- a/services/galley/test/integration/API/Federation.hs
+++ b/services/galley/test/integration/API/Federation.hs
@@ -1,21 +1,3 @@
--- This file is part of the Wire Server implementation.
---
--- Copyright (C) 2020 Wire Swiss GmbH
---
--- This program is free software: you can redistribute it and/or modify it under
--- the terms of the GNU Affero General Public License as published by the Free
--- Software Foundation, either version 3 of the License, or (at your option) any
--- later version.
---
--- This program is distributed in the hope that it will be useful, but WITHOUT
--- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
--- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
--- details.
---
--- You should have received a copy of the GNU Affero General Public License along
--- with this program. If not, see .
-{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
-
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 Wire Swiss GmbH
@@ -32,9 +14,12 @@
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
+{-# LANGUAGE RecordWildCards #-}
+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
module API.Federation where
+import API.MLS.Util
import API.Util
import Bilge
import Bilge.Assert
@@ -43,10 +28,11 @@ import Control.Lens hiding ((#))
import Data.Aeson (ToJSON (..))
import qualified Data.Aeson as A
import Data.ByteString.Conversion (toByteString')
+import Data.Default
import Data.Domain
import Data.Id (ConvId, Id (..), UserId, newClientId, randomId)
-import Data.Json.Util (Base64ByteString (..), toBase64Text)
-import Data.List.NonEmpty (NonEmpty (..))
+import Data.Json.Util hiding ((#))
+import Data.List.NonEmpty (NonEmpty (..), head)
import Data.List1
import qualified Data.List1 as List1
import qualified Data.Map as Map
@@ -106,7 +92,8 @@ tests s =
test s "POST /federation/on-message-sent : Receive a message from another backend" onMessageSent,
test s "POST /federation/send-message : Post a message sent from another backend" sendMessage,
test s "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" onUserDeleted,
- test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin
+ test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin,
+ test s "POST /federation/mls-welcome : Post an MLS welcome message received from another backend" sendMLSWelcome
]
getConversationsAllFound :: TestM ()
@@ -1158,6 +1145,29 @@ updateConversationByRemoteAdmin = do
let convUpdate :: ConversationUpdate = fromRight (error $ "Could not parse ConversationUpdate from " <> show (frBody rpc)) $ A.eitherDecode (frBody rpc)
pure (rpc, convUpdate)
+sendMLSWelcome :: TestM ()
+sendMLSWelcome = do
+ let aliceDomain = Domain "a.far-away.example.com"
+ -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain
+ MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {creatorOrigin = RemoteUser aliceDomain}
+ let bob = users !! 0
+ bobClient = snd . Data.List.NonEmpty.head . pClients $ bob
+
+ fedGalleyClient <- view tsFedGalleyClient
+ cannon <- view tsCannon
+
+ WS.bracketR cannon (qUnqualified (pUserId bob)) $ \wsB -> do
+ -- send welcome message
+ runFedClient @"mls-welcome" fedGalleyClient aliceDomain $
+ MLSWelcomeRequest
+ (Base64ByteString welcome)
+ [MLSWelcomeRecipient (qUnqualified . pUserId $ bob, bobClient)]
+
+ -- check that the corresponding event is received
+ void . liftIO $
+ WS.assertMatch (5 # WS.Second) wsB $
+ wsAssertMLSWelcome (pUserId bob) welcome
+
getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag)
getConvAction tquery (SomeConversationAction tag action) =
case (tag, tquery) of
diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs
index 84f45e9182e..5cf7e4eb77e 100644
--- a/services/galley/test/integration/API/MLS.hs
+++ b/services/galley/test/integration/API/MLS.hs
@@ -24,7 +24,9 @@ import API.Util
import Bilge
import Bilge.Assert
import Control.Lens (view)
+import qualified Data.Aeson as Aeson
import Data.Default
+import Data.Domain
import Data.Id
import qualified Data.List.NonEmpty as NonEmpty
import Data.List1
@@ -32,6 +34,7 @@ import Data.Qualified
import Data.Range
import Data.String.Conversions
import qualified Data.Text as T
+import Federator.MockServer
import Imports
import qualified Network.Wai.Utilities.Error as Wai
import System.FilePath
@@ -44,6 +47,7 @@ import TestHelpers
import TestSetup
import Wire.API.Conversation
import Wire.API.Conversation.Role
+import Wire.API.Federation.API.Galley
import Wire.API.Message
tests :: IO TestSetup -> TestTree
@@ -53,7 +57,8 @@ tests s =
[ testGroup
"Welcome"
[ test s "local welcome" testLocalWelcome,
- test s "local welcome (client with no public key)" testWelcomeNoKey
+ test s "local welcome (client with no public key)" testWelcomeNoKey,
+ test s "remote welcome" testRemoteWelcome
],
testGroup
"Creation"
@@ -108,22 +113,14 @@ postMLSConvOk = do
testLocalWelcome :: TestM ()
testLocalWelcome = do
- MessagingSetup {..} <- aliceInvitesBob 1 def
+ MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def
let bob = users !! 0
- galley <- viewGalley
cannon <- view tsCannon
WS.bracketR cannon (qUnqualified (pUserId bob)) $ \wsB -> do
-- send welcome message
- post
- ( galley
- . paths ["mls", "welcome"]
- . zUser (qUnqualified (pUserId creator))
- . zConn "conn"
- . content "message/mls"
- . bytes welcome
- )
+ postWelcome (qUnqualified $ pUserId creator) welcome
!!! const 201 === statusCode
-- check that the corresponding event is received
@@ -133,19 +130,42 @@ testLocalWelcome = do
testWelcomeNoKey :: TestM ()
testWelcomeNoKey = do
- MessagingSetup {..} <- aliceInvitesBob 1 def {createClients = CreateWithoutKey}
+ MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createClients = CreateWithoutKey}
- galley <- viewGalley
- post
- ( galley
- . paths ["mls", "welcome"]
- . zUser (qUnqualified (pUserId creator))
- . content "message/mls"
- . zConn "conn"
- . bytes welcome
- )
+ postWelcome (qUnqualified (pUserId creator)) welcome
!!! const 404 === statusCode
+testRemoteWelcome :: TestM ()
+testRemoteWelcome = do
+ -- 1. Create a conversation with Alice and Bob
+ let bobDomain = Domain "b.far-away.example.com"
+ opts = def {createConv = CreateConv, createClients = DontCreateClients}
+ MessagingSetup {..} <- aliceInvitesBob (1, RemoteUser bobDomain) opts
+ let alice = creator
+ bob = Imports.head users
+
+ let mockedResponse fedReq =
+ case frRPC fedReq of
+ "mls-welcome" -> pure (Aeson.encode ())
+ ms -> assertFailure ("unmocked endpoint called: " <> cs ms)
+
+ (_resp, reqs) <-
+ withTempMockFederator' mockedResponse $
+ postWelcome (qUnqualified $ pUserId alice) welcome
+ !!! const 201 === statusCode
+
+ -- Assert the correct federated call is made.
+ fedWelcome <- assertOne (filter ((== "mls-welcome") . frRPC) reqs)
+ let welcomeRequest :: Maybe MLSWelcomeRequest = Aeson.decode (frBody fedWelcome)
+ liftIO $
+ fmap mwrRecipients welcomeRequest
+ @?= Just
+ [ MLSWelcomeRecipient
+ ( qUnqualified . pUserId $ bob,
+ snd . NonEmpty.head . pClients $ bob
+ )
+ ]
+
-- | Send a commit message, and assert that all participants see an event with
-- the given list of new members.
testSuccessfulCommitWithNewUsers :: HasCallStack => MessagingSetup -> [Qualified UserId] -> TestM ()
@@ -213,7 +233,7 @@ testSuccessfulCommit setup = testSuccessfulCommitWithNewUsers setup (map pUserId
testAddUser :: TestM ()
testAddUser = do
- setup@MessagingSetup {..} <- aliceInvitesBob 1 def {createConv = CreateConv}
+ setup@MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createConv = CreateConv}
testSuccessfulCommit setup
-- check that bob can now see the conversation
@@ -228,7 +248,7 @@ testAddUser = do
testAddUserNotConnected :: TestM ()
testAddUserNotConnected = do
- setup@MessagingSetup {..} <- aliceInvitesBob 1 def {createConv = CreateConv, makeConnections = False}
+ setup@MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createConv = CreateConv, makeConnections = False}
let bob = users !! 0
-- try to add unconnected user
@@ -244,7 +264,7 @@ testAddUserWithProteusClients = do
setup <- withSystemTempDirectory "mls" $ \tmp -> do
(alice, users@[bob]) <- withLastPrekeys $ do
-- bob has 2 MLS clients
- participants@(_, [bob]) <- setupParticipants tmp def [2]
+ participants@(_, [bob]) <- setupParticipants tmp def [(2, LocalUser)]
-- and a non-MLS client
void $ takeLastPrekey >>= lift . randomClient (qUnqualified (pUserId bob))
@@ -263,7 +283,7 @@ testAddUserPartial :: TestM ()
testAddUserPartial = do
(creator, commit) <- withSystemTempDirectory "mls" $ \tmp -> do
-- Bob has 3 clients, Charlie has 2
- (alice, [bob, charlie]) <- withLastPrekeys $ setupParticipants tmp def [3, 2]
+ (alice, [bob, charlie]) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [3, 2])
void $ setupGroup tmp CreateConv alice "group"
(commit, _) <-
liftIO . setupCommit tmp "group" "group" $
@@ -289,7 +309,7 @@ testAddNewClient :: TestM ()
testAddNewClient = do
withSystemTempDirectory "mls" $ \tmp -> withLastPrekeys $ do
-- bob starts with a single client
- (creator, users@[bob]) <- setupParticipants tmp def [1]
+ (creator, users@[bob]) <- setupParticipants tmp def [(1, LocalUser)]
conversation <- lift $ setupGroup tmp CreateConv creator "group"
-- creator sends first commit message
@@ -299,7 +319,8 @@ testAddNewClient = do
do
-- then bob adds a new client
- bobC <- setupUserClient tmp CreateWithKey (pUserId bob)
+ (qcid, c) <- setupUserClient tmp CreateWithKey (pUserId bob)
+ let bobC = (qcid, c)
-- which gets added to the group
(commit, welcome) <- liftIO $ setupCommit tmp "group" "group" [bobC]
-- and the corresponding commit is sent
@@ -307,13 +328,13 @@ testAddNewClient = do
testAddUsersToProteus :: TestM ()
testAddUsersToProteus = do
- setup <- aliceInvitesBob 1 def {createConv = CreateProteusConv}
+ setup <- aliceInvitesBob (1, LocalUser) def {createConv = CreateProteusConv}
err <- testFailedCommit setup 404
liftIO $ Wai.label err @?= "no-conversation"
testAddUsersDirectly :: TestM ()
testAddUsersDirectly = do
- setup@MessagingSetup {..} <- aliceInvitesBob 1 def {createConv = CreateConv}
+ setup@MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createConv = CreateConv}
void $ postCommit setup
charlie <- randomUser
e <-
@@ -327,7 +348,7 @@ testAddUsersDirectly = do
testRemoveUsersDirectly :: TestM ()
testRemoveUsersDirectly = do
- setup@MessagingSetup {..} <- aliceInvitesBob 1 def {createConv = CreateConv}
+ setup@MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createConv = CreateConv}
void $ postCommit setup
e <-
responseJsonError
@@ -340,7 +361,7 @@ testRemoveUsersDirectly = do
testProteusMessage :: TestM ()
testProteusMessage = do
- setup@MessagingSetup {..} <- aliceInvitesBob 1 def {createConv = CreateConv}
+ setup@MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createConv = CreateConv}
void $ postCommit setup
e <-
responseJsonError
@@ -356,7 +377,7 @@ testProteusMessage = do
testStaleCommit :: TestM ()
testStaleCommit = withSystemTempDirectory "mls" $ \tmp -> do
- (creator, users) <- withLastPrekeys $ setupParticipants tmp def [2, 3]
+ (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [2, 3])
conversation <- setupGroup tmp CreateConv creator "group.0"
let (users1, users2) = splitAt 1 users
@@ -379,7 +400,7 @@ testStaleCommit = withSystemTempDirectory "mls" $ \tmp -> do
testAppMessage :: TestM ()
testAppMessage = withSystemTempDirectory "mls" $ \tmp -> do
- (creator, users) <- withLastPrekeys $ setupParticipants tmp def [1, 2, 3]
+ (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [1, 2, 3])
conversation <- setupGroup tmp CreateConv creator "group"
(commit, welcome) <-
diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs
index 25b2fade389..f3ecc9db920 100644
--- a/services/galley/test/integration/API/MLS/Util.hs
+++ b/services/galley/test/integration/API/MLS/Util.hs
@@ -23,6 +23,7 @@ import API.Util
import Bilge
import Bilge.Assert
import Control.Lens (preview, to, view)
+import Control.Monad.Catch
import qualified Control.Monad.State as State
import qualified Data.ByteString as BS
import Data.ByteString.Conversion
@@ -30,11 +31,11 @@ import Data.Default
import Data.Domain
import Data.Id
import Data.Json.Util
-import Data.List.NonEmpty (NonEmpty, nonEmpty)
+import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
-import Data.List1
import qualified Data.Map as Map
import Data.Qualified
+import qualified Data.Set as Set
import qualified Data.Text as T
import Imports
import System.FilePath
@@ -42,6 +43,7 @@ import System.IO.Temp
import System.Process
import Test.QuickCheck (arbitrary, generate)
import Test.Tasty.HUnit
+import TestHelpers
import TestSetup
import Wire.API.Conversation
import Wire.API.Conversation.Protocol
@@ -58,6 +60,8 @@ data CreateClients = CreateWithoutKey | CreateWithKey | DontCreateClients
data CreateConv = CreateConv | CreateProteusConv | DontCreateConv
deriving (Eq)
+data UserOrigin = LocalUser | RemoteUser Domain
+
createNewConv :: CreateConv -> Maybe NewConv
createNewConv CreateConv = Just defNewMLSConv
createNewConv CreateProteusConv = Just defNewProteusConv
@@ -65,6 +69,7 @@ createNewConv DontCreateConv = Nothing
data SetupOptions = SetupOptions
{ createClients :: CreateClients,
+ creatorOrigin :: UserOrigin,
createConv :: CreateConv,
makeConnections :: Bool
}
@@ -73,6 +78,7 @@ instance Default SetupOptions where
def =
SetupOptions
{ createClients = CreateWithKey,
+ creatorOrigin = LocalUser,
createConv = DontCreateConv,
makeConnections = True
}
@@ -106,6 +112,7 @@ setupUserClient ::
Qualified UserId ->
State.StateT [LastPrekey] TestM (String, ClientId)
setupUserClient tmp doCreateClients usr = do
+ localDomain <- lift viewFederationDomain
lpk <- takeLastPrekey
lift $ do
-- create client if requested
@@ -127,9 +134,23 @@ setupUserClient tmp doCreateClients usr = do
=<< spawn (cli tmp ["key-package", qcid]) Nothing
liftIO $ BS.writeFile (tmp > qcid) (rmRaw kp)
- -- set bob's private key and upload key package if required
+ -- Set Bob's private key and upload key package if required. If a client
+ -- does not have to be created and it is remote, pretend to have claimed its
+ -- key package.
case doCreateClients of
CreateWithKey -> addKeyPackage usr c kp
+ DontCreateClients | localDomain /= qDomain usr -> do
+ brig <- view tsBrig
+ let bundle =
+ KeyPackageBundle $
+ Set.singleton $
+ KeyPackageBundleEntry
+ { kpbeUser = usr,
+ kpbeClient = c,
+ kpbeRef = fromJust $ kpRef' kp,
+ kpbeKeyPackage = KeyPackageData $ rmRaw kp
+ }
+ mapRemoteKeyPackageRef brig bundle
_ -> pure ()
pure (qcid, c)
@@ -149,20 +170,36 @@ setupParticipants ::
HasCallStack =>
FilePath ->
SetupOptions ->
- [Int] ->
+ -- | A list of pairs, where each pair represents the number of clients for a
+ -- participant other than the group creator and whether the participant is
+ -- local or remote.
+ [(Int, UserOrigin)] ->
State.StateT [LastPrekey] TestM (Participant, [Participant])
setupParticipants tmp SetupOptions {..} ns = do
- creator <- lift randomQualifiedUser >>= setupParticipant tmp DontCreateClients 1
- others <- for ns $ \n ->
- lift randomQualifiedUser >>= setupParticipant tmp createClients n
- lift . when makeConnections $
- traverse_
- ( connectUsers (qUnqualified (pUserId creator))
- . List1
- . fmap (qUnqualified . pUserId)
- )
- (nonEmpty others)
- pure (creator, others)
+ creator <- lift (createUserOrId creatorOrigin) >>= setupParticipant tmp DontCreateClients 1
+ others <- for ns $ \(n, ur) ->
+ lift (createUserOrId ur) >>= fmap (,ur) . setupParticipant tmp createClients n
+ lift . when makeConnections $ do
+ for_ others $ \(o, ur) -> case (creatorOrigin, ur) of
+ (LocalUser, LocalUser) ->
+ connectUsers (qUnqualified (pUserId creator)) (pure ((qUnqualified . pUserId) o))
+ (LocalUser, RemoteUser _) ->
+ connectWithRemoteUser
+ (qUnqualified . pUserId $ creator)
+ (pUserId o)
+ (RemoteUser _, LocalUser) ->
+ connectWithRemoteUser
+ (qUnqualified . pUserId $ o)
+ (pUserId creator)
+ (RemoteUser _, RemoteUser _) ->
+ liftIO $
+ assertFailure "Trying to have both the creator and a group participant remote"
+ pure (creator, fst <$> others)
+ where
+ createUserOrId :: UserOrigin -> TestM (Qualified UserId)
+ createUserOrId = \case
+ LocalUser -> randomQualifiedUser
+ RemoteUser d -> randomQualifiedId d
withLastPrekeys :: Monad m => State.StateT [LastPrekey] m a -> m a
withLastPrekeys m = State.evalStateT m someLastPrekeys
@@ -218,11 +255,12 @@ takeLastPrekey = do
State.put lpks
pure lpk
--- | Setup: Alice creates a group and invites bob. Return welcome and commit message.
-aliceInvitesBob :: HasCallStack => Int -> SetupOptions -> TestM MessagingSetup
-aliceInvitesBob numBobClients opts@SetupOptions {..} = withSystemTempDirectory "mls" $ \tmp -> do
- (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp opts [numBobClients]
-
+-- | Setup: Alice creates a group and invites Bob that is local or remote to
+-- Alice depending on the passed in creator origin. Return welcome and commit
+-- message.
+aliceInvitesBob :: HasCallStack => (Int, UserOrigin) -> SetupOptions -> TestM MessagingSetup
+aliceInvitesBob bobConf opts@SetupOptions {..} = withSystemTempDirectory "mls" $ \tmp -> do
+ (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp opts [bobConf]
-- create a group
conversation <- setupGroup tmp createConv alice "group"
@@ -269,6 +307,24 @@ addKeyPackage u c kp = do
(Request -> Request) -> KeyPackageBundle -> m ()
+mapRemoteKeyPackageRef brig bundle =
+ void $
+ put
+ ( brig
+ . paths ["i", "mls", "key-package-refs"]
+ . json bundle
+ )
+ !!! const 204 === statusCode
+
+claimKeyPackage :: (MonadIO m, MonadHttp m) => (Request -> Request) -> UserId -> Qualified UserId -> m ResponseLBS
+claimKeyPackage brig claimant target =
+ post
+ ( brig
+ . paths ["mls", "key-packages", "claim", toByteString' (qDomain target), toByteString' (qUnqualified target)]
+ . zUser claimant
+ )
+
postCommit :: MessagingSetup -> TestM [Event]
postCommit MessagingSetup {..} = do
galley <- viewGalley
@@ -281,3 +337,15 @@ postCommit MessagingSetup {..} = do
. bytes commit
)
UserId -> ByteString -> m ResponseLBS
+postWelcome uid welcome = do
+ galley <- viewGalley
+ post
+ ( galley
+ . paths ["mls", "welcome"]
+ . zUser uid
+ . zConn "conn"
+ . content "message/mls"
+ . bytes welcome
+ )