From 1db37c940b8df5dd54f0fc1bb77600d61f04ada2 Mon Sep 17 00:00:00 2001 From: jschaul Date: Wed, 2 Jun 2021 19:08:08 +0200 Subject: [PATCH] Federation: new endpoint: GET /conversations/{domain}/{cnv} (#1566) --- .../src/Wire/API/Federation/Error.hs | 7 +++ .../src/Wire/API/Routes/Public/Galley.hs | 9 ++++ services/galley/src/Galley/API/Public.hs | 3 +- services/galley/src/Galley/API/Query.hs | 36 ++++++++++++-- services/galley/test/integration/API.hs | 49 ++++++++++++++++++- services/galley/test/integration/API/Util.hs | 14 ++++++ 6 files changed, 112 insertions(+), 6 deletions(-) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index a4b08614b6d..1c455b84fc1 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -88,6 +88,13 @@ federationInvalidBody msg = "federation-invalid-body" ("Could not parse remote federator response: " <> LT.fromStrict msg) +federationUnexpectedBody :: Text -> Wai.Error +federationUnexpectedBody msg = + Wai.Error + unexpectedFederationResponseStatus + "federation-unexpected-body" + ("Could parse body, but response was not expected: " <> LT.fromStrict msg) + federationNotConfigured :: Wai.Error federationNotConfigured = Wai.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 4a32ede0e24..6fba2d295fc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -21,6 +21,7 @@ module Wire.API.Routes.Public.Galley where import Data.CommaSeparatedList +import Data.Domain import Data.Id (ConvId, TeamId) import Data.Range import Data.Swagger @@ -54,11 +55,19 @@ instance ToSchema Servant.NoContent where data Api routes = Api { -- Conversations + getUnqualifiedConversation :: + routes + :- Summary "Get a conversation by ID" + :> ZUser + :> "conversations" + :> Capture "cnv" ConvId + :> Get '[Servant.JSON] Public.Conversation, getConversation :: routes :- Summary "Get a conversation by ID" :> ZUser :> "conversations" + :> Capture "domain" Domain :> Capture "cnv" ConvId :> Get '[Servant.JSON] Public.Conversation, getConversationRoles :: diff --git a/services/galley/src/Galley/API/Public.hs b/services/galley/src/Galley/API/Public.hs index aacbf62b215..574ddb1e2b7 100644 --- a/services/galley/src/Galley/API/Public.hs +++ b/services/galley/src/Galley/API/Public.hs @@ -76,7 +76,8 @@ servantSitemap :: ServerT GalleyAPI.ServantAPI Galley servantSitemap = genericServerT $ GalleyAPI.Api - { GalleyAPI.getConversation = Query.getConversation, + { GalleyAPI.getUnqualifiedConversation = Query.getUnqualifiedConversation, + GalleyAPI.getConversation = Query.getConversation, GalleyAPI.getConversationRoles = Query.getConversationRoles, GalleyAPI.getConversationIds = Query.getConversationIds, GalleyAPI.getConversations = Query.getConversations, diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 4531f6861c6..505460cebfc 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -17,6 +17,7 @@ module Galley.API.Query ( getBotConversationH, + getUnqualifiedConversation, getConversation, getConversationRoles, getConversationIds, @@ -27,11 +28,13 @@ module Galley.API.Query ) where +import Control.Error (runExceptT) +import Control.Monad.Catch (throwM) import Data.CommaSeparatedList import Data.Domain (Domain) import Data.Id as Id import Data.Proxy -import Data.Qualified (Qualified (Qualified)) +import Data.Qualified (Qualified (..)) import Data.Range import Galley.API.Error import qualified Galley.API.Mapping as Mapping @@ -48,6 +51,10 @@ import Network.Wai.Predicate hiding (result, setStatus) import Network.Wai.Utilities import qualified Wire.API.Conversation as Public import qualified Wire.API.Conversation.Role as Public +import Wire.API.Federation.API.Galley (gcresConvs) +import qualified Wire.API.Federation.API.Galley as FederatedGalley +import Wire.API.Federation.Client (executeFederated) +import Wire.API.Federation.Error import qualified Wire.API.Provider.Bot as Public getBotConversationH :: BotId ::: ConvId ::: JSON -> Galley Response @@ -68,11 +75,34 @@ getBotConversation zbot zcnv = do | otherwise = Just (OtherMember (Qualified (memId m) domain) (memService m) (memConvRoleName m)) -getConversation :: UserId -> ConvId -> Galley Public.Conversation -getConversation zusr cnv = do +getUnqualifiedConversation :: UserId -> ConvId -> Galley Public.Conversation +getUnqualifiedConversation zusr cnv = do c <- getConversationAndCheckMembership zusr cnv Mapping.conversationView zusr c +getConversation :: UserId -> Domain -> ConvId -> Galley Public.Conversation +getConversation zusr domain cnv = do + localDomain <- viewFederationDomain + if domain == localDomain + then getUnqualifiedConversation zusr cnv + else getRemoteConversation zusr (Qualified cnv domain) + +getRemoteConversation :: UserId -> Qualified ConvId -> Galley Public.Conversation +getRemoteConversation zusr (Qualified convId remoteDomain) = do + localDomain <- viewFederationDomain + let qualifiedZUser = Qualified zusr localDomain + req = FederatedGalley.GetConversationsRequest qualifiedZUser [convId] + rpc = FederatedGalley.getConversations FederatedGalley.clientRoutes req + -- we expect the remote galley to make adequate checks on conversation + -- membership and just pass through the reponse + conversations <- + runExceptT (executeFederated remoteDomain rpc) + >>= either (throwM . federationErrorToWai) pure + case gcresConvs conversations of + [] -> throwM convNotFound + [conv] -> pure conv + _convs -> throwM (federationUnexpectedBody "expected one conversation, got multiple") + getConversationRoles :: UserId -> ConvId -> Galley Public.ConversationRolesList getConversationRoles zusr cnv = do void $ getConversationAndCheckMembership zusr cnv diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 23ac09adbd1..ddbb7efc61e 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -66,6 +66,7 @@ import TestHelpers import TestSetup import Util.Options (Endpoint (Endpoint)) import Wire.API.Conversation.Member (Member (..)) +import Wire.API.Federation.API.Galley (GetConversationsResponse (GetConversationsResponse)) import Wire.API.User.Client (UserClientPrekeyMap, getUserClientPrekeyMap) tests :: IO TestSetup -> TestTree @@ -117,6 +118,7 @@ tests s = test s "fail to add members when not connected" postMembersFail, test s "fail to add too many members" postTooManyMembersFail, test s "add remote members" testAddRemoteMember, + test s "get remote conversation" testGetRemoteConversation, test s "add non-existing remote members" testAddRemoteMemberFailure, test s "add deleted remote members" testAddDeletedRemoteUser, test s "add remote members on invalid domain" testAddRemoteMemberInvalidDomain, @@ -876,7 +878,9 @@ leaveConnectConversation = do -- See also the comment in Galley.API.Update.addMembers for some other checks that are necessary. testAddRemoteMember :: TestM () testAddRemoteMember = do - alice <- randomUser + aliceQ <- randomQualifiedUser + let alice = qUnqualified aliceQ + let localDomain = qDomain aliceQ bobId <- randomId let remoteDomain = Domain "far-away.example.com" remoteBob = Qualified bobId remoteDomain @@ -897,12 +901,53 @@ testAddRemoteMember = do -- FUTUREWORK: implement returning remote users in the event. -- evtData e @?= Just (EdMembersJoin (SimpleMembers [remoteBob])) evtFrom e @?= alice - conv <- responseJsonUnsafeWithMsg "conversation" <$> getConv alice convId + conv <- responseJsonUnsafeWithMsg "conversation" <$> getConvQualified alice (Qualified convId localDomain) liftIO $ do let actual = cmOthers $ cnvMembers conv let expected = [OtherMember remoteBob Nothing roleNameWireAdmin] assertEqual "other members should include remoteBob" expected actual +testGetRemoteConversation :: TestM () +testGetRemoteConversation = do + aliceQ <- randomQualifiedUser + let alice = qUnqualified aliceQ + bobId <- randomId + convId <- randomId + let remoteDomain = Domain "far-away.example.com" + remoteConv = Qualified convId remoteDomain + + let aliceAsOtherMember = OtherMember aliceQ Nothing roleNameWireAdmin + bobAsMember = Member bobId Nothing False Nothing Nothing False Nothing False Nothing roleNameWireAdmin + remoteConversationResponse = + GetConversationsResponse + [ Conversation + { cnvId = convId, + cnvType = RegularConv, + cnvCreator = alice, + cnvAccess = [], + cnvAccessRole = ActivatedAccessRole, + cnvName = Just "federated gossip", + cnvMembers = ConvMembers bobAsMember [aliceAsOtherMember], + cnvTeam = Nothing, + cnvMessageTimer = Nothing, + cnvReceiptMode = Nothing + } + ] + opts <- view tsGConf + g <- view tsGalley + (resp, _) <- + liftIO $ + withTempMockFederator + opts + remoteDomain + (const remoteConversationResponse) + (getConvQualified' g alice remoteConv) + conv :: Conversation <- responseJsonUnsafe <$> (pure resp Qualified ConvId -> TestM ResponseLBS +getConvQualified u convId = do + g <- view tsGalley + getConvQualified' g u convId + +getConvQualified' :: (MonadIO m, MonadHttp m) => GalleyR -> UserId -> Qualified ConvId -> m ResponseLBS +getConvQualified' g u (Qualified conv domain) = do + get $ + g + . paths ["conversations", toByteString' domain, toByteString' conv] + . zUser u + . zConn "conn" + . zType "access" + getConvIds :: UserId -> Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> TestM ResponseLBS getConvIds u r s = do g <- view tsGalley