Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GET endpoint for MLS 1-1 conversations #3345

Merged
merged 7 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1-api-changes/get-mls-one2one
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new endpoint `GET /conversations/one2one/:domain/:uid` to fetch the MLS 1-1 conversation with another user
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ library
Test.Demo
Test.MLS
Test.MLS.KeyPackage
Test.MLS.One2One
Test.User
Testlib.App
Testlib.Assertions
Expand Down
44 changes: 44 additions & 0 deletions integration/test/API/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ import qualified Data.Text.Encoding as T
import GHC.Stack
import Testlib.Prelude

data AddUser = AddUser
{ name :: Maybe String,
email :: Maybe String,
teamCode :: Maybe String,
password :: Maybe String
}

instance Default AddUser where
def = AddUser Nothing Nothing Nothing Nothing

addUser :: (HasCallStack, MakesValue dom) => dom -> AddUser -> App Response
addUser dom opts = do
req <- baseRequest dom Brig Versioned "register"
name <- maybe randomName pure opts.name
submit "POST" $
req
& addJSONObject
[ "name" .= name,
"email" .= opts.email,
"team_code" .= opts.teamCode,
"password" .= fromMaybe defPassword opts.password
]

getUser ::
(HasCallStack, MakesValue user, MakesValue target) =>
user ->
Expand Down Expand Up @@ -213,3 +236,24 @@ putUserSupportedProtocols user ps = do
baseRequest user Brig Versioned $
joinHttpPath ["self", "supported-protocols"]
submit "PUT" (req & addJSONObject ["supported_protocols" .= ps])

data PostInvitation = PostInvitation
{ email :: Maybe String
}

instance Default PostInvitation where
def = PostInvitation Nothing

postInvitation ::
(HasCallStack, MakesValue user) =>
user ->
PostInvitation ->
App Response
postInvitation user inv = do
tid <- user %. "team" & asString
req <-
baseRequest user Brig Versioned $
joinHttpPath ["teams", tid, "invitations"]
email <- maybe randomEmail pure inv.email
submit "POST" $
req & addJSONObject ["email" .= email]
9 changes: 9 additions & 0 deletions integration/test/API/BrigInternal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,12 @@ deleteOAuthClient user cid = do
clientId <- objId cid
req <- baseRequest user Brig Unversioned $ "i/oauth/clients/" <> clientId
submit "DELETE" req

getInvitationCode :: (HasCallStack, MakesValue user, MakesValue inv) => user -> inv -> App Response
getInvitationCode user inv = do
tid <- user %. "team" & asString
invId <- inv %. "id" & asString
req <-
baseRequest user Brig Unversioned $
"i/teams/invitation-code?team=" <> tid <> "&invitation_id=" <> invId
submit "GET" req
10 changes: 7 additions & 3 deletions integration/test/API/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ defPassword :: String
defPassword = "hunter2!"

randomEmail :: App String
randomEmail = liftIO $ do
n <- randomRIO (8, 15)
u <- replicateM n pick
randomEmail = do
u <- randomName
pure $ u <> "@example.com"

randomName :: App String
randomName = liftIO $ do
n <- randomRIO (8, 15)
replicateM n pick
where
chars :: Array.Array Int Char
chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9']
Expand Down
12 changes: 12 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,15 @@ deleteTeamConv team conv user = do
convId <- objId conv
req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId])
submit "DELETE" req

getMLSOne2OneConversation ::
(HasCallStack, MakesValue self, MakesValue other) =>
self ->
other ->
App Response
getMLSOne2OneConversation self other = do
(domain, uid) <- objQid other
req <-
baseRequest self Galley Versioned $
joinHttpPath ["conversations", "one2one", domain, uid]
submit "GET" req
8 changes: 8 additions & 0 deletions integration/test/SetupHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,11 @@ supportMLS u = do
let prots' = "mls" : prots
bindResponse (putUserSupportedProtocols u prots') $ \resp ->
resp.status `shouldMatchInt` 200

addUserToTeam :: (HasCallStack, MakesValue u) => u -> App Value
addUserToTeam u = do
inv <- postInvitation u def >>= getJSON 201
email <- inv %. "email" & asString
resp <- getInvitationCode u inv >>= getJSON 200
code <- resp %. "code" & asString
addUser u def {email = Just email, teamCode = Just code} >>= getJSON 201
43 changes: 43 additions & 0 deletions integration/test/Test/MLS/One2One.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Test.MLS.One2One where

import API.Galley
import SetupHelpers
import Testlib.Prelude

testGetMLSOne2One :: HasCallStack => Domain -> App ()
testGetMLSOne2One otherDomain = do
[alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain]

conv <- getMLSOne2OneConversation alice bob >>= getJSON 200

conv %. "type" `shouldMatchInt` 2
others <- conv %. "members.others" & asList
other <- assertOne others
other %. "conversation_role" `shouldMatch` "wire_member"
other %. "qualified_id" `shouldMatch` (bob %. "qualified_id")

conv %. "members.self.conversation_role" `shouldMatch` "wire_member"
conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id")

convId <- conv %. "qualified_id"

-- check that the conversation has the same ID on the other side
conv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json

conv2 %. "type" `shouldMatchInt` 2
conv2 %. "qualified_id" `shouldMatch` convId

testGetMLSOne2OneUnconnected :: HasCallStack => Domain -> App ()
testGetMLSOne2OneUnconnected otherDomain = do
[alice, bob] <- for [OwnDomain, otherDomain] $ \domain -> randomUser domain def

bindResponse (getMLSOne2OneConversation alice bob) $ \resp ->
resp.status `shouldMatchInt` 403

testGetMLSOne2OneSameTeam :: App ()
testGetMLSOne2OneSameTeam = do
(alice, _) <- createTeam OwnDomain
bob <- addUserToTeam alice
void $ getMLSOne2OneConversation alice bob >>= getJSON 200
14 changes: 8 additions & 6 deletions libs/galley-types/src/Galley/Types/Conversations/One2One.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Data.UUID (UUID)
import qualified Data.UUID as UUID
import qualified Data.UUID.Tagged as U
import Imports
import Wire.API.User

-- | The hash function used to obtain the 1-1 conversation ID for a pair of users.
--
Expand All @@ -39,8 +40,9 @@ hash = convert . Crypto.hash @ByteString @Crypto.SHA256

-- | A randomly-generated UUID to use as a namespace for the UUIDv5 of 1-1
-- conversation IDs
namespace :: UUID
namespace = UUID.fromWords 0x9a51edb8 0x060c0d9a 0x0c2950a8 0x5d152982
namespace :: BaseProtocolTag -> UUID
namespace BaseProtocolProteusTag = UUID.fromWords 0x9a51edb8 0x060c0d9a 0x0c2950a8 0x5d152982
namespace BaseProtocolMLSTag = UUID.fromWords 0x95589dd5 0xb04540dc 0xa6aadd9c 0x4fad1c2f

compareDomains :: Ord a => Qualified a -> Qualified a -> Ordering
compareDomains (Qualified a1 dom1) (Qualified a2 dom2) =
Expand Down Expand Up @@ -88,13 +90,13 @@ quidToByteString (Qualified uid domain) = toByteString' uid <> toByteString' dom
-- the most significant bit of the octet at index 16) is 0, and B otherwise.
-- This is well-defined, because we assumed the number of bits of x to be
-- strictly larger than 128.
one2OneConvId :: Qualified UserId -> Qualified UserId -> Qualified ConvId
one2OneConvId a b = case compareDomains a b of
GT -> one2OneConvId b a
one2OneConvId :: BaseProtocolTag -> Qualified UserId -> Qualified UserId -> Qualified ConvId
one2OneConvId protocol a b = case compareDomains a b of
GT -> one2OneConvId protocol b a
_ ->
let c =
mconcat
[ L.toStrict (UUID.toByteString namespace),
[ L.toStrict (UUID.toByteString (namespace protocol)),
quidToByteString a,
quidToByteString b
]
Expand Down
24 changes: 24 additions & 0 deletions libs/wire-api/src/Wire/API/Conversation/Member.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ module Wire.API.Conversation.Member

-- * Member
Member (..),
defMember,
MutedStatus (..),
OtherMember (..),
defOtherMember,

-- * Member Update
MemberUpdate (..),
Expand Down Expand Up @@ -88,6 +90,20 @@ data Member = Member
deriving (Arbitrary) via (GenericUniform Member)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema Member

defMember :: Qualified UserId -> Member
defMember uid =
Member
{ memId = uid,
memService = Nothing,
memOtrMutedStatus = Nothing,
memOtrMutedRef = Nothing,
memOtrArchived = False,
memOtrArchivedRef = Nothing,
memHidden = False,
memHiddenRef = Nothing,
memConvRoleName = roleNameWireMember
}

instance ToSchema Member where
schema =
object "Member" $
Expand Down Expand Up @@ -133,6 +149,14 @@ data OtherMember = OtherMember
deriving (Arbitrary) via (GenericUniform OtherMember)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema OtherMember

defOtherMember :: Qualified UserId -> OtherMember
defOtherMember uid =
OtherMember
{ omQualifiedId = uid,
omService = Nothing,
omConvRoleName = roleNameWireMember
}

instance ToSchema OtherMember where
schema =
object "OtherMember" $
Expand Down
11 changes: 11 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,17 @@ type ConversationAPI =
:> ReqBody '[JSON] NewConv
:> ConversationVerb
)
:<|> Named
"get-one-to-one-mls-conversation"
( Summary "Get an MLS 1:1 conversation"
:> ZLocalUser
:> CanThrow 'MLSNotEnabled
:> CanThrow 'NotConnected
:> "conversations"
:> "one2one"
:> QualifiedCapture "usr" UserId
:> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" Conversation)
)
-- This endpoint can lead to the following events being sent:
-- - MemberJoin event to members
:<|> Named
Expand Down
2 changes: 1 addition & 1 deletion services/brig/test/integration/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ localAndRemoteUserWithConvId brig shouldBeLocal = do
quid <- userQualifiedId <$> randomUser brig
let go = do
other <- Qualified <$> randomId <*> pure (Domain "far-away.example.com")
let convId = one2OneConvId quid other
let convId = one2OneConvId BaseProtocolProteusTag quid other
isLocal = qDomain quid == qDomain convId
if shouldBeLocal == isLocal
then pure (qUnqualified quid, other, convId)
Expand Down
4 changes: 2 additions & 2 deletions services/galley/src/Galley/API/Action.hs
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,10 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do
<$> E.selectTeamMembers tid newUsers
let userMembershipMap = map (id &&& flip Map.lookup tms) newUsers
ensureAccessRole (convAccessRoles conv) userMembershipMap
ensureConnectedOrSameTeam lusr newUsers
ensureConnectedToLocalsOrSameTeam lusr newUsers
checkLocals lusr Nothing newUsers = do
ensureAccessRole (convAccessRoles conv) (zip newUsers $ repeat Nothing)
ensureConnectedOrSameTeam lusr newUsers
ensureConnectedToLocalsOrSameTeam lusr newUsers

checkRemotes ::
( Member BrigAccess r,
Expand Down
3 changes: 2 additions & 1 deletion services/galley/src/Galley/API/Create.hs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import Wire.API.Team
import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented))
import Wire.API.Team.Member
import Wire.API.Team.Permission hiding (self)
import Wire.API.User

----------------------------------------------------------------------------
-- Group conversations
Expand Down Expand Up @@ -422,7 +423,7 @@ createOne2OneConversationUnchecked self zcon name mtid other = do
self
createOne2OneConversationLocally
createOne2OneConversationRemotely
create (one2OneConvId (tUntagged self) other) self zcon name mtid other
create (one2OneConvId BaseProtocolProteusTag (tUntagged self) other) self zcon name mtid other

createOne2OneConversationLocally ::
( Member ConversationStore r,
Expand Down
12 changes: 10 additions & 2 deletions services/galley/src/Galley/API/One2One.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import Galley.Types.UserList
import Imports
import Polysemy
import Wire.API.Conversation hiding (Member)
import Wire.API.Routes.Internal.Galley.ConversationsIntra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..))
import Wire.API.Routes.Internal.Galley.ConversationsIntra
import Wire.API.User

newConnectConversationWithRemote ::
Local UserId ->
Expand All @@ -59,7 +60,14 @@ iUpsertOne2OneConversation ::
UpsertOne2OneConversationRequest ->
Sem r UpsertOne2OneConversationResponse
iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do
let convId = fromMaybe (one2OneConvId (tUntagged uooLocalUser) (tUntagged uooRemoteUser)) uooConvId
let convId =
fromMaybe
( one2OneConvId
BaseProtocolProteusTag
(tUntagged uooLocalUser)
(tUntagged uooRemoteUser)
)
uooConvId

let dolocal :: Local ConvId -> Sem r ()
dolocal lconvId = do
Expand Down
1 change: 1 addition & 0 deletions services/galley/src/Galley/API/Public/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ conversationAPI =
<@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo)
<@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation)
<@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation)
<@> mkNamedAPI @"get-one-to-one-mls-conversation" getMLSOne2OneConversation
<@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified)
<@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2)
<@> mkNamedAPI @"add-members-to-conversation" (callsFed addMembers)
Expand Down
Loading