diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/brig/templates/tests/configmap.yaml index 002bc004a2..1842519cd7 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -61,6 +61,10 @@ data: host: brig.{{ .Release.Namespace }}-fed2.svc.cluster.local port: 8080 + galley: + host: galley.{{ .Release.Namespace }}-fed2.svc.cluster.local + port: 8080 + # TODO remove this federator: host: federator.{{ .Release.Namespace }}-fed2.svc.cluster.local diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index 880d34e26a..246b5bb5e0 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -90,6 +90,7 @@ modelConversationMembers = Doc.defineModel "ConversationMembers" $ do -------------------------------------------------------------------------------- -- Members +-- FUTUREWORK: Add a qualified Id here. data Member = Member { memId :: UserId, memService :: Maybe ServiceRef, diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 871d066371..4bd6fd1114 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -20,7 +20,7 @@ module Federation.End2end where import API.Search.Util import API.User.Util (getUserClientsQualified) import Bilge -import Bilge.Assert ((!!!), (===)) +import Bilge.Assert ((!!!), ( Manager -> Brig -> Galley -> Endpoint -> Brig -> IO TestTree -spec _brigOpts mg brig galley _federator brigTwo = +spec :: BrigOpts.Opts -> Manager -> Brig -> Galley -> Endpoint -> Brig -> Galley -> IO TestTree +spec _brigOpts mg brig galley _federator brigTwo galleyTwo = pure $ testGroup "federation-end2end-user" @@ -71,7 +71,7 @@ spec _brigOpts mg brig galley _federator brigTwo = test mg "claim prekey bundle" $ testClaimPrekeyBundleSuccess brig brigTwo, test mg "claim multi-prekey bundle" $ testClaimMultiPrekeyBundleSuccess brig brigTwo, test mg "list user clients" $ testListUserClients brig brigTwo, - test mg "add remote users to local conversation" $ testAddRemoteUsersToLocalConv brig galley brigTwo + test mg "add remote users to local conversation" $ testAddRemoteUsersToLocalConv brig galley brigTwo galleyTwo ] -- | Path covered by this test: @@ -210,12 +210,12 @@ testClaimMultiPrekeyBundleSuccess brig1 brig2 = do const 200 === statusCode const (Just ucm) === responseJsonMaybe -testAddRemoteUsersToLocalConv :: Brig -> Galley -> Brig -> Http () -testAddRemoteUsersToLocalConv brig1 galley1 brig2 = do +testAddRemoteUsersToLocalConv :: Brig -> Galley -> Brig -> Galley -> Http () +testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do alice <- randomUser brig1 bob <- randomUser brig2 - let conv = NewConvUnmanaged $ NewConv [] [] (Just "gossip") mempty Nothing Nothing Nothing Nothing roleNameWireAdmin + let newConv = NewConvUnmanaged $ NewConv [] [] (Just "gossip") mempty Nothing Nothing Nothing Nothing roleNameWireAdmin convId <- cnvId . responseJsonUnsafe <$> post @@ -224,9 +224,13 @@ testAddRemoteUsersToLocalConv brig1 galley1 brig2 = do . zUser (userId alice) . zConn "conn" . header "Z-Type" "access" - . json conv + . json newConv ) + let backend1Domain = qDomain (userQualifiedId alice) + -- FUTUREWORK add qualified conversation Id to Conversation data type, then use that from the conversation creation response + qualifiedConvId = Qualified convId backend1Domain + let invite = InviteQualified (userQualifiedId bob :| []) roleNameWireAdmin post ( galley1 @@ -238,6 +242,21 @@ testAddRemoteUsersToLocalConv brig1 galley1 brig2 = do ) !!! (const 200 === statusCode) + -- test GET /conversations/:backend1Domain/:cnv + liftIO $ putStrLn "search for conversation on backend 1..." + res <- getConvQualified galley1 (userId alice) qualifiedConvId Brig -> Http () testListUserClients brig1 brig2 = do alice <- randomUser brig1 diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index 16bd6345e4..d118e2ba27 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -24,6 +24,7 @@ module Federation.Util where import Bilge +import Bilge.Assert ((!!!), ( ExceptT a m b -> m b assertRightT = assertRight <=< runExceptT + +getConvQualified :: Galley -> UserId -> Qualified ConvId -> Http ResponseLBS +getConvQualified g u (Qualified cnvId domain) = + get $ + g + . paths ["conversations", toByteString' domain, toByteString' cnvId] + . zUser u + . zConn "conn" + . header "Z-Type" "access" diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index 01ef86e6b4..4ea9bc1b36 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -63,6 +63,7 @@ import Util.Test data BackendConf = BackendConf { remoteBrig :: Endpoint, + remoteGalley :: Endpoint, remoteFederatorInternal :: Endpoint, remoteFederatorExternal :: Endpoint } @@ -72,6 +73,7 @@ instance FromJSON BackendConf where parseJSON = withObject "BackendConf" $ \o -> BackendConf <$> o .: "brig" + <*> o .: "galley" <*> o .: "federatorInternal" <*> o .: "federatorExternal" @@ -105,6 +107,7 @@ runTests iConf brigOpts otherArgs = do s = mkRequest $ spar iConf f = federatorInternal iConf brigTwo = mkRequest $ remoteBrig (backendTwo iConf) + galleyTwo = mkRequest $ remoteGalley (backendTwo iConf) let turnFile = Opts.servers . Opts.turn $ brigOpts turnFileV2 = (Opts.serversV2 . Opts.turn) brigOpts @@ -129,7 +132,7 @@ runTests iConf brigOpts otherArgs = do createIndex <- Index.Create.spec brigOpts browseTeam <- TeamUserSearch.tests brigOpts mg g b userPendingActivation <- UserPendingActivation.tests brigOpts mg db b g s - federationEnd2End <- Federation.End2end.spec brigOpts mg b g f brigTwo + federationEnd2End <- Federation.End2end.spec brigOpts mg b g f brigTwo galleyTwo federationEndpoints <- API.Federation.tests mg b fedBrigClient includeFederationTests <- (== Just "1") <$> Blank.getEnv "INTEGRATION_FEDERATION_TESTS" internalApi <- API.Internal.tests brigOpts mg b (brig iConf) gd diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 8b2b0dabc1..bf2bafb47b 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 63f50e23b5853d8dd4b8b87b8588dc8499783ec5e7e72a181b0ccb399089a6ed +-- hash: 9fb2b9ba716dce4ffe27891b8718f10b22ca443bf73c84744b150c53aae5a5c1 name: galley version: 0.83.0 @@ -377,6 +377,7 @@ test-suite galley-types-tests other-modules: Test.Galley.API Test.Galley.Intra.User + Test.Galley.Mapping Test.Galley.Roundtrip Paths_galley hs-source-dirs: diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index cad95e4e5e..9b8487f5e4 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -39,12 +39,11 @@ federationSitemap = } getConversations :: GetConversationsRequest -> Galley GetConversationsResponse -getConversations (GetConversationsRequest (Qualified uid domain) gcrConvIds) = do - localDomain <- viewFederationDomain +getConversations (GetConversationsRequest qUid gcrConvIds) = do + domain <- viewFederationDomain convs <- Data.conversations gcrConvIds - if domain == localDomain - then GetConversationsResponse . catMaybes <$> for convs (Mapping.conversationViewMaybe uid) - else error "FUTUREWORK: implement & exstend integration test when schema ready" + let convViews = Mapping.conversationViewMaybeQualified domain qUid <$> convs + pure $ GetConversationsResponse . catMaybes $ convViews -- FUTUREWORK: also remove users from conversation updateConversationMemberships :: ConversationMemberUpdate -> Galley () diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index 5dfd45a373..6321a86916 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -23,7 +23,7 @@ import Control.Monad.Catch import Data.Domain (Domain) import Data.Id (UserId, idToText) import qualified Data.List as List -import Data.Qualified (Qualified (Qualified)) +import Data.Qualified (Qualified (..)) import Data.Tagged (unTagged) import Galley.API.Util (viewFederationDomain) import Galley.App @@ -53,17 +53,30 @@ conversationView uid conv = do throwM badState badState = mkError status500 "bad-state" "Bad internal member state." +conversationViewMaybe :: UserId -> Data.Conversation -> Galley (Maybe Public.Conversation) +conversationViewMaybe u conv = do + localDomain <- viewFederationDomain + pure $ conversationViewMaybeQualified localDomain (Qualified u localDomain) conv + -- | View for a given user of a stored conversation. -- Returns 'Nothing' when the user is not part of the conversation. -conversationViewMaybe :: UserId -> Data.Conversation -> Galley (Maybe Public.Conversation) -conversationViewMaybe u Data.Conversation {..} = do - domain <- viewFederationDomain - let (me, localThem) = List.partition ((u ==) . Internal.memId) convLocalMembers - let localMembers = localToOther domain <$> localThem +conversationViewMaybeQualified :: Domain -> Qualified UserId -> Data.Conversation -> Maybe Public.Conversation +conversationViewMaybeQualified localDomain qUid Data.Conversation {..} = do + let localMembers = localToOther localDomain <$> convLocalMembers let remoteMembers = remoteToOther <$> convRemoteMembers - for (listToMaybe me) $ \m -> do - let mems = Public.ConvMembers (toMember m) (localMembers <> remoteMembers) - return $! Public.Conversation convId convType convCreator convAccess convAccessRole convName mems convTeam convMessageTimer convReceiptMode + let me = List.find ((qUid ==) . Public.omQualifiedId) (localMembers <> remoteMembers) + let otherMembers = filter ((qUid /=) . Public.omQualifiedId) (localMembers <> remoteMembers) + let userAndConvOnSameBackend = find ((qUnqualified qUid ==) . Internal.memId) convLocalMembers + let selfMember = + -- if the user and the conversation are on the same backend, we can create a real self member + -- otherwise, we need to fall back to a default self member (see futurework) + -- (Note: the extra domain check is done to catch the edge case where two users in a conversation have the same unqualified UUID) + if isJust userAndConvOnSameBackend && localDomain == qDomain qUid + then toMember <$> userAndConvOnSameBackend + else incompleteSelfMember <$> me + selfMember <&> \m -> do + let mems = Public.ConvMembers m otherMembers + Public.Conversation convId convType convCreator convAccess convAccessRole convName mems convTeam convMessageTimer convReceiptMode where localToOther :: Domain -> Internal.LocalMember -> Public.OtherMember localToOther domain x = @@ -81,6 +94,23 @@ conversationViewMaybe u Data.Conversation {..} = do Public.omConvRoleName = Internal.rmConvRoleName x } + -- FUTUREWORK(federation): we currently don't store muted, archived etc status for users who are on a different backend than a conversation + -- but we should. Once this information is available, the code should be changed to use the stored information, rather than these defaults. + incompleteSelfMember :: Public.OtherMember -> Public.Member + incompleteSelfMember m = + Public.Member + { memId = qUnqualified (Public.omQualifiedId m), + memService = Nothing, + memOtrMuted = False, + memOtrMutedStatus = Nothing, + memOtrMutedRef = Nothing, + memOtrArchived = False, + memOtrArchivedRef = Nothing, + memHidden = False, + memHiddenRef = Nothing, + memConvRoleName = Public.omConvRoleName m + } + toMember :: Internal.LocalMember -> Public.Member toMember x@Internal.InternalMember {..} = Public.Member {memId = Internal.memId x, ..} diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 8860f04170..29821dfea9 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1029,11 +1029,11 @@ testAddRemoteMember = do ] e <- responseJsonUnsafe <$> (pure resp getConvQualified alice qconvId liftIO $ do diff --git a/services/galley/test/unit/Main.hs b/services/galley/test/unit/Main.hs index fe750244bf..9c53c62d29 100644 --- a/services/galley/test/unit/Main.hs +++ b/services/galley/test/unit/Main.hs @@ -23,6 +23,7 @@ where import Imports import qualified Test.Galley.API import qualified Test.Galley.Intra.User +import qualified Test.Galley.Mapping import qualified Test.Galley.Roundtrip import Test.Tasty @@ -32,5 +33,6 @@ main = =<< sequence [ pure Test.Galley.API.tests, pure Test.Galley.Intra.User.tests, + pure Test.Galley.Mapping.tests, Test.Galley.Roundtrip.tests ] diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs new file mode 100644 index 0000000000..7ed75e2df3 --- /dev/null +++ b/services/galley/test/unit/Test/Galley/Mapping.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- 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 . + +module Test.Galley.Mapping where + +import Data.Domain +import Data.Id +import Data.Qualified +import Galley.API () +import Galley.API.Mapping +import qualified Galley.Data as Data +import Galley.Types (LocalMember, RemoteMember) +import qualified Galley.Types.Conversations.Members as I +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import Wire.API.Conversation +import Wire.API.Conversation.Role (roleNameWireAdmin) + +tests :: TestTree +tests = + testGroup + "ConversationMapping" + [ testCase "Alice@A Conv@A" runMappingSimple, + testCase "Alice@A Conv@A requester=not a member@A" runMappingNotAMemberA, + testCase "Alice@A Conv@A requester=not a member@B" runMappingNotAMemberB, + testCase "Alice@A Conv@A Bob@B" runMappingRemoteUser, + testCase "Alice@A Conv@B Bob@B" runMappingRemoteConv, + testCase "Alice@A Conv@B Bob@B bobUUID=aliceUUID" runMappingSameUnqualifiedUUID + ] + +runMappingSimple :: HasCallStack => IO () +runMappingSimple = do + let convDomain = Domain "backendA.example.com" + let userDomain = Domain "backendA.example.com" + alice <- randomId + let requester = Qualified alice userDomain + let expectedSelf = Just $ mkMember requester + let expectedOthers = Just [] + + let locals = [mkInternalMember requester] + let remotes = [] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv + + assertEqual "self:" expectedSelf (cmSelf <$> actual) + assertEqual "others:" expectedOthers (cmOthers <$> actual) + +runMappingNotAMemberA :: HasCallStack => IO () +runMappingNotAMemberA = do + let convDomain = Domain "backendA.example.com" + let aliceDomain = Domain "backendA.example.com" + alice <- flip Qualified aliceDomain <$> randomId + requester <- flip Qualified aliceDomain <$> randomId + + let locals = [mkInternalMember alice] + let remotes = [] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv + + assertEqual "members:" Nothing actual + +runMappingNotAMemberB :: HasCallStack => IO () +runMappingNotAMemberB = do + let convDomain = Domain "backendA.example.com" + let aliceDomain = Domain "backendA.example.com" + let requesterDomain = Domain "backendB.example.com" + alice <- flip Qualified aliceDomain <$> randomId + requester <- flip Qualified requesterDomain <$> randomId + + let locals = [mkInternalMember alice] + let remotes = [] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv + + assertEqual "members:" Nothing actual + +runMappingRemoteUser :: HasCallStack => IO () +runMappingRemoteUser = do + let aliceDomain = Domain "backendA.example.com" + let convDomain = Domain "backendA.example.com" + let bobDomain = Domain "backendB.example.com" + alice <- flip Qualified aliceDomain <$> randomId + bob <- flip Qualified bobDomain <$> randomId + let expectedSelf = Just $ mkMember alice + let expectedOthers = Just [mkOtherMember bob] + + let locals = [mkInternalMember alice] + let remotes = [mkRemoteMember bob] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv + + assertEqual "self:" expectedSelf (cmSelf <$> actual) + assertEqual "others:" expectedOthers (cmOthers <$> actual) + +runMappingRemoteConv :: HasCallStack => IO () +runMappingRemoteConv = do + let aliceDomain = Domain "backendA.example.com" + let convDomain = Domain "backendB.example.com" + let bobDomain = Domain "backendB.example.com" + alice <- flip Qualified aliceDomain <$> randomId + bob <- flip Qualified bobDomain <$> randomId + let expectedSelf = Just $ mkMember alice + let expectedOthers = Just [mkOtherMember bob] + + let locals = [mkInternalMember bob] + let remotes = [mkRemoteMember alice] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv + + assertEqual "self:" expectedSelf (cmSelf <$> actual) + assertEqual "others:" expectedOthers (cmOthers <$> actual) + +-- Here we expect the conversationView to return nothing, because Alice (the +-- requester) is not part of the conversation (Her unqualified UUID is part of +-- the conversation, but the function should catch this possibly malicious +-- edge case) +runMappingSameUnqualifiedUUID :: HasCallStack => IO () +runMappingSameUnqualifiedUUID = do + let aliceDomain = Domain "backendA.example.com" + let convDomain = Domain "backendB.example.com" + let bobDomain = Domain "backendB.example.com" + uuid <- randomId + let alice = Qualified uuid aliceDomain + let bob = Qualified uuid bobDomain + + let locals = [mkInternalMember bob] + let remotes = [] + conv <- mkInternalConv locals remotes + let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv + + assertEqual "members:" Nothing actual + +-------------------------------------------------------------- + +mkOtherMember :: Qualified UserId -> OtherMember +mkOtherMember u = OtherMember u Nothing roleNameWireAdmin + +mkRemoteMember :: Qualified UserId -> RemoteMember +mkRemoteMember u = I.RemoteMember (toRemote u) roleNameWireAdmin + +mkInternalConv :: [LocalMember] -> [RemoteMember] -> IO Data.Conversation +mkInternalConv locals remotes = do + -- for the conversationView unit tests, the creator plays no importance, so for simplicity this is set to a random value. + creator <- randomId + cnv <- randomId + pure $ + Data.Conversation + { Data.convId = cnv, + Data.convType = RegularConv, + Data.convCreator = creator, + Data.convName = Just "unit testing gossip", + Data.convAccess = [], + Data.convAccessRole = ActivatedAccessRole, + Data.convLocalMembers = locals, + Data.convRemoteMembers = remotes, + Data.convTeam = Nothing, + Data.convDeleted = Just False, + Data.convMessageTimer = Nothing, + Data.convReceiptMode = Nothing + } + +mkMember :: Qualified UserId -> Member +mkMember (Qualified userId _domain) = + Member + { memId = userId, + memService = Nothing, + memOtrMuted = False, + memOtrMutedStatus = Nothing, + memOtrMutedRef = Nothing, + memOtrArchived = False, + memOtrArchivedRef = Nothing, + memHidden = False, + memHiddenRef = Nothing, + memConvRoleName = roleNameWireAdmin + } + +mkInternalMember :: Qualified UserId -> LocalMember +mkInternalMember (Qualified userId _domain) = + I.InternalMember + { I.memId = userId, + I.memService = Nothing, + I.memOtrMuted = False, + I.memOtrMutedStatus = Nothing, + I.memOtrMutedRef = Nothing, + I.memOtrArchived = False, + I.memOtrArchivedRef = Nothing, + I.memHidden = False, + I.memHiddenRef = Nothing, + I.memConvRoleName = roleNameWireAdmin + } diff --git a/services/integration.yaml b/services/integration.yaml index a4d7f6309d..ea5bd5667e 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -60,6 +60,9 @@ backendTwo: brig: host: 127.0.0.1 # in kubernetes, brig..svc.cluster.local port: 9082 + galley: + host: 127.0.0.1 # in kubernetes, galley..svc.cluster.local + port: 9085 federatorInternal: host: 127.0.0.1 # in kubernetes, federator..svc.cluster.local port: 9097