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 + )