Skip to content

Commit

Permalink
Add qualified broadcast endpoint (#2166)
Browse files Browse the repository at this point in the history
* Add qualified broadcast endpoint

* Refactor broadcast tests

This uses a record to organise arguments for the helper function.

* Add qualified broadcast tests

* Fix mismatch assertions in broadcast tests

* Generalise broadcast route in nginz configuration

* Add release note entry

* Restore authentication for broadcast in demo nginz

* Remove redundant use of `runLocalInput`

* Rename broadcast tests
  • Loading branch information
pcapriotti committed Mar 3, 2022
1 parent ee8a992 commit a55abd7
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 130 deletions.
1 change: 1 addition & 0 deletions changelog.d/0-release-notes/nginz-upgrade
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
For wire.com operators: make sure that nginz is deployed
1 change: 1 addition & 0 deletions changelog.d/1-api-changes/broadcast-qualified
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add qualified broadcast endpoint
2 changes: 1 addition & 1 deletion charts/nginz/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ nginx_conf:
- all
max_body_size: 40m
body_buffer_size: 256k
- path: /broadcast/otr/messages
- path: /broadcast
envs:
- all
max_body_size: 40m
Expand Down
2 changes: 1 addition & 1 deletion deploy/services-demo/conf/nginz/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ http {
proxy_pass http://galley;
}

location /broadcast/otr/messages {
location /broadcast {
include common_response_with_zauth.conf;
proxy_pass http://galley;
}
Expand Down
21 changes: 20 additions & 1 deletion libs/wire-api/src/Wire/API/Routes/Public/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,25 @@ type MessagingAPI =
(PostOtrResponses MessageSendingStatus)
(Either (MessageNotSent MessageSendingStatus) MessageSendingStatus)
)
:<|> Named
"post-proteus-broadcast"
( Summary "Post an encrypted message to all team members and all contacts (accepts only Protobuf)"
:> Description PostOtrDescription
:> ZLocalUser
:> ZConn
:> CanThrow TeamNotFound
:> CanThrow BroadcastLimitExceeded
:> CanThrow NonBindingTeam
:> "broadcast"
:> "proteus"
:> "messages"
:> ReqBody '[Proto] QualifiedNewOtrMessage
:> MultiVerb
'POST
'[JSON]
(PostOtrResponses MessageSendingStatus)
(Either (MessageNotSent MessageSendingStatus) MessageSendingStatus)
)

type BotAPI =
Named
Expand Down Expand Up @@ -1048,7 +1067,7 @@ type PostOtrDescription =
\- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n\
\- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\
\\n\
\The sending of messages in a federated conversation could theorectically fail partially. \
\The sending of messages in a federated conversation could theoretically fail partially. \
\To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. \
\So, if any backend is down, the message is not propagated to anyone. \
\But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, \
Expand Down
1 change: 1 addition & 0 deletions services/galley/galley.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ executable galley-integration
, containers
, cookie
, currency-codes
, data-default
, data-timeout
, errors
, exceptions
Expand Down
1 change: 1 addition & 0 deletions services/galley/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ executables:
- cookie
- currency-codes
- metrics-wai
- data-default
- data-timeout
- errors
- exceptions
Expand Down
1 change: 1 addition & 0 deletions services/galley/src/Galley/API/Public/Servant.hs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ servantSitemap = conversations :<|> teamConversations :<|> messaging :<|> bot :<
Named @"post-otr-message-unqualified" postOtrMessageUnqualified
:<|> Named @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified
:<|> Named @"post-proteus-message" postProteusMessage
:<|> Named @"post-proteus-broadcast" postProteusBroadcast

bot =
Named @"post-bot-message-unqualified" postBotMessageUnqualified
Expand Down
25 changes: 25 additions & 0 deletions services/galley/src/Galley/API/Update.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module Galley.API.Update
-- * Talking
postProteusMessage,
postOtrMessageUnqualified,
postProteusBroadcast,
postOtrBroadcastUnqualified,
isTypingUnqualified,

Expand Down Expand Up @@ -1114,6 +1115,30 @@ postProteusMessage sender zcon conv msg = runLocalInput sender $ do
(\c -> postRemoteOtrMessage (qUntagged sender) c (rpRaw msg))
conv

postProteusBroadcast ::
Members
'[ BotAccess,
BrigAccess,
ClientStore,
ConversationStore,
Error ActionError,
Error TeamError,
FederatorAccess,
GundeckAccess,
ExternalAccess,
Input Opts,
Input UTCTime,
MemberStore,
TeamStore,
TinyLog
]
r =>
Local UserId ->
ConnId ->
QualifiedNewOtrMessage ->
Sem r (PostOtrResponse MessageSendingStatus)
postProteusBroadcast sender zcon msg = postBroadcast sender (Just zcon) msg

unqualifyEndpoint ::
Functor f =>
Local x ->
Expand Down
6 changes: 3 additions & 3 deletions services/galley/test/integration/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysProto = do
conv <- decodeConvId <$> postConv alice [bob, eve] (Just "gossip") [] Nothing Nothing
-- Missing eve
let ciphertext = toBase64Text "hello bob"
let m = otrRecipients [(bob, [(bc, ciphertext)])]
let m = otrRecipients [(bob, bc, ciphertext)]
r1 <-
postProtoOtrMessage alice ac conv m
<!! const 412 === statusCode
Expand Down Expand Up @@ -501,7 +501,7 @@ postCryptoMessageNotAuthorizeUnknownClient = do
conv <- decodeConvId <$> postConv alice [bob] (Just "gossip") [] Nothing Nothing
-- Unknown client ID => 403
let ciphertext = toBase64Text "hello bob"
let m = otrRecipients [(bob, [(bc, ciphertext)])]
let m = otrRecipients [(bob, bc, ciphertext)]
postProtoOtrMessage alice (ClientId "172618352518396") conv m
!!! const 403 === statusCode

Expand Down Expand Up @@ -576,7 +576,7 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do
conv <- decodeConvId <$> postConv alice [bob, chad, eve] (Just "gossip") [] Nothing Nothing
-- Missing eve
let msgMissingChadAndEve = [(bob, bc, toBase64Text "hello bob")]
let m' = otrRecipients [(bob, [(bc, toBase64Text "hello bob")])]
let m' = otrRecipients [(bob, bc, toBase64Text "hello bob")]
-- These three are equivalent (i.e. report all missing clients)
postOtrMessage id alice ac conv msgMissingChadAndEve
!!! const 412 === statusCode
Expand Down
135 changes: 54 additions & 81 deletions services/galley/test/integration/API/Teams.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import Data.ByteString.Conversion
import Data.ByteString.Lazy (fromStrict)
import Data.Csv (FromNamedRecord (..), decodeByName)
import qualified Data.Currency as Currency
import Data.Default
import Data.Id
import Data.Json.Util hiding ((#))
import qualified Data.LegalHold as LH
Expand Down Expand Up @@ -135,13 +136,23 @@ tests s =
test s "team tests around truncation limits - no events, too large team" (testTeamAddRemoveMemberAboveThresholdNoEvents >> ensureQueueEmpty),
test s "send billing events to owners even in large teams" testBillingInLargeTeam,
test s "send billing events to some owners in large teams (indexedBillingTeamMembers disabled)" testBillingInLargeTeamWithoutIndexedBillingTeamMembers,
test s "post crypto broadcast message json" postCryptoBroadcastMessageJson,
test s "post crypto broadcast message json - filtered only, too large team" postCryptoBroadcastMessageJsonFilteredTooLargeTeam,
test s "post crypto broadcast message json (report missing in body)" postCryptoBroadcastMessageJsonReportMissingBody,
test s "post crypto broadcast message protobuf" postCryptoBroadcastMessageProto,
test s "post crypto broadcast message redundant/missing" postCryptoBroadcastMessageJson2,
test s "post crypto broadcast message no-team" postCryptoBroadcastMessageNoTeam,
test s "post crypto broadcast message 100 (or max conns)" postCryptoBroadcastMessage100OrMaxConns
testGroup "broadcast" $
[ (BroadcastLegacyBody, BroadcastJSON),
(BroadcastLegacyQueryParams, BroadcastJSON),
(BroadcastLegacyBody, BroadcastProto),
(BroadcastQualified, BroadcastProto)
]
<&> \(api, ty) ->
let bcast = def {bAPI = api, bType = ty}
in testGroup
(broadcastAPIName api <> " - " <> broadcastTypeName ty)
[ test s "message" (postCryptoBroadcastMessage bcast),
test s "filtered only, too large team" (postCryptoBroadcastMessageFilteredTooLargeTeam bcast),
test s "report missing in body" (postCryptoBroadcastMessageReportMissingBody bcast),
test s "redundant/missing" (postCryptoBroadcastMessage2 bcast),
test s "no-team" (postCryptoBroadcastMessageNoTeam bcast),
test s "100 (or max conns)" (postCryptoBroadcastMessage100OrMaxConns bcast)
]
]

timeout :: WS.Timeout
Expand Down Expand Up @@ -1610,8 +1621,8 @@ testUpdateTeamStatus = do
const 403 === statusCode
const "invalid-team-status-update" === (Error.label . responseJsonUnsafeWithMsg "error label")

postCryptoBroadcastMessageJson :: TestM ()
postCryptoBroadcastMessageJson = do
postCryptoBroadcastMessage :: Broadcast -> TestM ()
postCryptoBroadcastMessage bcast = do
localDomain <- viewFederationDomain
let q :: Id a -> Qualified (Id a)
q = (`Qualified` localDomain)
Expand Down Expand Up @@ -1642,9 +1653,9 @@ postCryptoBroadcastMessageJson = do
-- Alice's clients 1 and 2 listen to their own messages only
WS.bracketR (c . queryItem "client" (toByteString' ac2)) alice $ \wsA2 ->
WS.bracketR (c . queryItem "client" (toByteString' ac)) alice $ \wsA1 -> do
Util.postOtrBroadcastMessage id alice ac msg !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = msg} !!! do
const 201 === statusCode
assertMismatch [] [] []
assertBroadcastMismatch localDomain (bAPI bcast) [] [] []
-- Bob should get the broadcast (team member of alice)
void . liftIO $
WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext1"))
Expand All @@ -1660,13 +1671,12 @@ postCryptoBroadcastMessageJson = do
void . liftIO $
WS.assertMatch t wsA2 (wsAssertOtr (q (selfConv alice)) (q alice) ac ac2 (toBase64Text "ciphertext0"))

postCryptoBroadcastMessageJsonFilteredTooLargeTeam :: TestM ()
postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do
postCryptoBroadcastMessageFilteredTooLargeTeam :: Broadcast -> TestM ()
postCryptoBroadcastMessageFilteredTooLargeTeam bcast = do
localDomain <- viewFederationDomain
let q :: Id a -> Qualified (Id a)
q = (`Qualified` localDomain)
opts <- view tsGConf
g <- view tsCannon
c <- view tsCannon
-- Team1: alice, bob and 3 unnamed
(alice, tid) <- Util.createBindingTeam
Expand Down Expand Up @@ -1705,14 +1715,14 @@ postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do
& optSettings . setMaxConvSize .~ 4
withSettingsOverrides newOpts $ do
-- Untargeted, Alice's team is too large
Util.postOtrBroadcastMessage' g Nothing id alice ac msg !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = msg} !!! do
const 400 === statusCode
const "too-many-users-to-broadcast" === Error.label . responseJsonUnsafeWithMsg "error label"
-- We target the message to the 4 users, that should be fine
let inbody = Just [alice, bob, charlie, dan]
Util.postOtrBroadcastMessage' g inbody id alice ac msg !!! do
Util.postBroadcast (q alice) ac bcast {bReport = inbody, bMessage = msg} !!! do
const 201 === statusCode
assertMismatch [] [] []
assertBroadcastMismatch localDomain (bAPI bcast) [] [] []
-- Bob should get the broadcast (team member of alice)
void . liftIO $
WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext1"))
Expand All @@ -1728,23 +1738,26 @@ postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do
void . liftIO $
WS.assertMatch t wsA2 (wsAssertOtr (q (selfConv alice)) (q alice) ac ac2 (toBase64Text "ciphertext0"))

postCryptoBroadcastMessageJsonReportMissingBody :: TestM ()
postCryptoBroadcastMessageJsonReportMissingBody = do
g <- view tsGalley
postCryptoBroadcastMessageReportMissingBody :: Broadcast -> TestM ()
postCryptoBroadcastMessageReportMissingBody bcast = do
localDomain <- viewFederationDomain
(alice, tid) <- Util.createBindingTeam
let qalice = Qualified alice localDomain
bob <- view userId <$> Util.addUserToTeam alice tid
_bc <- Util.randomClient bob (someLastPrekeys !! 1) -- this is important!
assertQueue "add bob" $ tUpdate 2 [alice]
refreshIndex
ac <- Util.randomClient alice (someLastPrekeys !! 0)
let inbody = Just [bob] -- body triggers report
inquery = (queryItem "report_missing" (toByteString' alice)) -- query doesn't
let -- add extraneous query parameter (unless using query parameter API)
inquery = case bAPI bcast of
BroadcastLegacyQueryParams -> id
_ -> queryItem "report_missing" (toByteString' alice)
msg = [(alice, ac, "ciphertext0")]
Util.postOtrBroadcastMessage' g inbody inquery alice ac msg
Util.postBroadcast qalice ac bcast {bReport = Just [bob], bMessage = msg, bReq = inquery}
!!! const 412 === statusCode

postCryptoBroadcastMessageJson2 :: TestM ()
postCryptoBroadcastMessageJson2 = do
postCryptoBroadcastMessage2 :: Broadcast -> TestM ()
postCryptoBroadcastMessage2 bcast = do
localDomain <- viewFederationDomain
let q :: Id a -> Qualified (Id a)
q = (`Qualified` localDomain)
Expand All @@ -1763,15 +1776,15 @@ postCryptoBroadcastMessageJson2 = do
let t = 3 # Second -- WS receive timeout
-- Missing charlie
let m1 = [(bob, bc, toBase64Text "ciphertext1")]
Util.postOtrBroadcastMessage id alice ac m1 !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = m1} !!! do
const 412 === statusCode
assertMismatchWithMessage (Just "1: Only Charlie and his device") [(charlie, Set.singleton cc)] [] []
assertBroadcastMismatch localDomain (bAPI bcast) [(charlie, Set.singleton cc)] [] []
-- Complete
WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do
let m2 = [(bob, bc, toBase64Text "ciphertext2"), (charlie, cc, toBase64Text "ciphertext2")]
Util.postOtrBroadcastMessage id alice ac m2 !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = m2} !!! do
const 201 === statusCode
assertMismatchWithMessage (Just "No devices expected") [] [] []
assertBroadcastMismatch localDomain (bAPI bcast) [] [] []
void . liftIO $
WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext2"))
void . liftIO $
Expand All @@ -1783,9 +1796,9 @@ postCryptoBroadcastMessageJson2 = do
(bob, bc, toBase64Text "ciphertext3"),
(charlie, cc, toBase64Text "ciphertext3")
]
Util.postOtrBroadcastMessage id alice ac m3 !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = m3} !!! do
const 201 === statusCode
assertMismatchWithMessage (Just "2: Only Alice and her device") [] [(alice, Set.singleton ac)] []
assertBroadcastMismatch localDomain (bAPI bcast) [] [(alice, Set.singleton ac)] []
void . liftIO $
WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext3"))
void . liftIO $
Expand All @@ -1796,66 +1809,26 @@ postCryptoBroadcastMessageJson2 = do
WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do
deleteClient charlie cc (Just defPassword) !!! const 200 === statusCode
let m4 = [(bob, bc, toBase64Text "ciphertext4"), (charlie, cc, toBase64Text "ciphertext4")]
Util.postOtrBroadcastMessage id alice ac m4 !!! do
Util.postBroadcast (q alice) ac bcast {bMessage = m4} !!! do
const 201 === statusCode
assertMismatchWithMessage (Just "3: Only Charlie and his device") [] [] [(charlie, Set.singleton cc)]
assertBroadcastMismatch localDomain (bAPI bcast) [] [] [(charlie, Set.singleton cc)]
void . liftIO $
WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext4"))
-- charlie should not get it
assertNoMsg wsE (wsAssertOtr (q (selfConv charlie)) (q alice) ac cc (toBase64Text "ciphertext4"))

postCryptoBroadcastMessageProto :: TestM ()
postCryptoBroadcastMessageProto = do
postCryptoBroadcastMessageNoTeam :: Broadcast -> TestM ()
postCryptoBroadcastMessageNoTeam bcast = do
localDomain <- viewFederationDomain
let q :: Id a -> Qualified (Id a)
q = (`Qualified` localDomain)
-- similar to postCryptoBroadcastMessageJson, postCryptoBroadcastMessageJsonReportMissingBody except uses protobuf

c <- view tsCannon
-- Team1: Alice, Bob. Team2: Charlie. Regular user: Dan. Connect Alice,Charlie,Dan
(alice, tid) <- Util.createBindingTeam
bob <- view userId <$> Util.addUserToTeam alice tid
assertQueue "add bob" $ tUpdate 2 [alice]
refreshIndex
(charlie, _) <- Util.createBindingTeam
refreshIndex
ac <- Util.randomClient alice (someLastPrekeys !! 0)
bc <- Util.randomClient bob (someLastPrekeys !! 1)
cc <- Util.randomClient charlie (someLastPrekeys !! 2)
(dan, dc) <- randomUserWithClient (someLastPrekeys !! 3)
connectUsers alice (list1 charlie [dan])
-- Complete: Alice broadcasts a message to Bob,Charlie,Dan
let t = 1 # Second -- WS receive timeout
let ciphertext = toBase64Text "hello bob"
WS.bracketRN c [alice, bob, charlie, dan] $ \ws@[_, wsB, wsC, wsD] -> do
let msg = otrRecipients [(bob, [(bc, ciphertext)]), (charlie, [(cc, ciphertext)]), (dan, [(dc, ciphertext)])]
Util.postProtoOtrBroadcast alice ac msg !!! do
const 201 === statusCode
assertMismatch [] [] []
-- Bob should get the broadcast (team member of alice)
void . liftIO $ WS.assertMatch t wsB (wsAssertOtr' (toBase64Text "data") (q (selfConv bob)) (q alice) ac bc ciphertext)
-- Charlie should get the broadcast (contact of alice and user of teams feature)
void . liftIO $ WS.assertMatch t wsC (wsAssertOtr' (toBase64Text "data") (q (selfConv charlie)) (q alice) ac cc ciphertext)
-- Dan should get the broadcast (contact of alice and not user of teams feature)
void . liftIO $ WS.assertMatch t wsD (wsAssertOtr' (toBase64Text "data") (q (selfConv dan)) (q alice) ac dc ciphertext)
-- Alice should not get her own broadcast
WS.assertNoEvent timeout ws
let inbody = Just [bob] -- body triggers report
inquery = (queryItem "report_missing" (toByteString' alice)) -- query doesn't
msg = otrRecipients [(alice, [(ac, ciphertext)])]
Util.postProtoOtrBroadcast' inbody inquery alice ac msg
!!! const 412 === statusCode

postCryptoBroadcastMessageNoTeam :: TestM ()
postCryptoBroadcastMessageNoTeam = do
(alice, ac) <- randomUserWithClient (someLastPrekeys !! 0)
let qalice = Qualified alice localDomain
(bob, bc) <- randomUserWithClient (someLastPrekeys !! 1)
connectUsers alice (list1 bob [])
let msg = [(bob, bc, toBase64Text "ciphertext1")]
Util.postOtrBroadcastMessage id alice ac msg !!! const 404 === statusCode
Util.postBroadcast qalice ac bcast {bMessage = msg} !!! const 404 === statusCode

postCryptoBroadcastMessage100OrMaxConns :: TestM ()
postCryptoBroadcastMessage100OrMaxConns = do
postCryptoBroadcastMessage100OrMaxConns :: Broadcast -> TestM ()
postCryptoBroadcastMessage100OrMaxConns bcast = do
localDomain <- viewFederationDomain
c <- view tsCannon
(alice, ac) <- randomUserWithClient (someLastPrekeys !! 0)
Expand All @@ -1868,9 +1841,9 @@ postCryptoBroadcastMessage100OrMaxConns = do
WS.bracketRN c (bob : (fst <$> others)) $ \ws -> do
let f (u, clt) = (u, clt, toBase64Text "ciphertext")
let msg = (bob, bc, toBase64Text "ciphertext") : (f <$> others)
Util.postOtrBroadcastMessage id alice ac msg !!! do
Util.postBroadcast qalice ac bcast {bMessage = msg} !!! do
const 201 === statusCode
assertMismatch [] [] []
assertBroadcastMismatch localDomain (bAPI bcast) [] [] []
let qbobself = Qualified (selfConv bob) localDomain
void . liftIO $
WS.assertMatch t (Imports.head ws) (wsAssertOtr qbobself qalice ac bc (toBase64Text "ciphertext"))
Expand Down
Loading

0 comments on commit a55abd7

Please sign in to comment.